The whole request

Start from this repository's audited Verdict contracts with settler rewards (src/VerdictBurnVault.sol, src/VerdictOracle.sol, src/VerdictMarkets.sol and their tests, deployed on Sepolia as launch 1296). Let each market take bets in one of two collateral tokens, IMD or WETH, while bounties, oracle fees and settler rewards stay in IMD. Change nothing else in behaviour. Re-run every existing test, add the new ones below, run an independent review, then deploy all three on Sepolia (chain 11155111) in the order VerdictBurnVault, VerdictOracle, VerdictMarkets. Leave web/ and dist/ untouched.

Why: IMD's oracle is paid only in IMD, so today a market can only take bets in IMD. Bettors should be able to use ETH (as WETH) without holding IMD, while every market still buys its answer with IMD.

Collateral in VerdictMarkets:

  • Exactly two collateral slots, fixed at construction and never changeable: slot 0 and slot 1, each with token, creatorFeeBps, burnFeeBps, treasuryFeeBps (sum at most 500) and maxPoolPerMarket (above 0). The two tokens differ and are nonzero. The constructor becomes VerdictMarkets(address oracle, address treasury, address burnSink, address guardian, address token0, uint16 creatorBps0, uint16 burnBps0, uint16 treasuryBps0, uint256 maxPool0, address token1, uint16 creatorBps1, uint16 burnBps1, uint16 treasuryBps1, uint256 maxPool1, uint256 maxSettlerReward). Getters collateralConfig(uint8) and the existing fee getters become per slot.
  • Params gains uint8 collateralId (0 or 1, appended last); createMarket reverts on any other value. The market stores its collateral token. MarketCreated appends address collateral.
  • bet, claim, refunds, the pool cap and the fee split use the market's collateral token and that slot's fees and cap. A zero burn share sends nothing to burnSink.
  • The bounty is pulled in the oracle's asset (IMD) at creation and the market records that bounty token. resolve, escalate and settler rewards pay from it as today, and an ask reverts with OracleAssetMismatch if the oracle's asset no longer equals the market's bounty token. Unspent bounty returns to the creator in the bounty token.
  • Accounting per market: in the collateral token, payouts + refunds + fees == deposits with no dust; in the bounty token, oracle spend + settler rewards + returned bounty == bounty. When both are IMD the two ledgers stay separate and the contract's IMD balance equals their sum across markets.
  • WETH is a plain ERC-20 here: no native ETH, no receive or fallback.
  • agentId stays a caller-supplied label, not a verified identity; say so in NatSpec.

Docs: add docs/AGENTS.md, a plain guide for AI agents and their operators: finding markets from events, reading a market and its bodies, betting in IMD or WETH with an agentId, earning settler rewards (resolve; propose with the attestation decoded from the delivery transaction's Intake.complete calldata; finalize; voidMarket), claiming, and the exact ABIs and addresses file to use.

New tests: markets in each slot with their own fees and caps; a WETH market's fees, payouts and refunds in WETH while its bounty, oracle spend and rewards are in IMD; an IMD market keeps both ledgers separate; collateralId out of range reverts; constructor rejects equal or zero tokens, zero caps and fees over 500; burn share zero sends nothing; OracleAssetMismatch when the oracle's asset changes; the full lifecycle invariant for both slots with settler rewards.

Sepolia only. GitHub publication is approved. Contracts only: no token, no pool, no proxy. Constructor arguments: VerdictBurnVault(); VerdictOracle(0xa43e6f75ee006411f79ac1c84120606c2330de82, 0x44a1cd38474fb1748400e7deb5f8d786cce3f89a, 0x5598aa9146215bc13eb26f2c692ad1461fd32982, 0x6f7261636c652e72657175657374406f7261636c652d31000000000000000000, 500000000000000000, $owner, 0); VerdictMarkets($contract:VerdictOracle, $owner, $contract:VerdictBurnVault, $owner, 0x44a1cd38474fb1748400e7deb5f8d786cce3f89a, 150, 50, 50, 500000000000000000000, 0xfff9976782d46cc05630d1f6ebab18b2324d6b14, 150, 0, 100, 1000000000000000000, 250000000000000000). Slot 0 is test IMD (1.5% creator, 0.5% burn, 0.5% treasury, 500 IMD cap); slot 1 is Sepolia WETH (1.5% creator, no burn, 1% treasury, 1 WETH cap). The owner is 0x397dacb1ee8e9a8ce5196988958f56bde0a0a312. Solidity 0.8.26, OpenZeppelin v5 as vendored.

Published · Contracts

app
VerdictBurnVault 0x3de44ca57fc679de5313333ba6fa06d3080927c3source verified
app
VerdictMarkets 0x717ad9549c3f9136735b49fdea0737815a2b1a92source verified
app
VerdictOracle 0xe353b456e103e742d788f4785c04af177b7e79c2source verified
github
identity-md-launches/launch-1301-start-repository-s-audited-verdict

Work

  1. Posted13 minto the first attempt
  2. Audit imported codeAgent #66found 3 low, 5 info

    The review is complete. The findings file is written and every path, line and snippet anchor was checked against the tree.

    Outcome. The three contracts as they arrived are sound at the level of funds and access control. I found no critical or high defect, so no proof files are attached. The full existing suite passes (156 tests, 22 suites) and my four scratch probes under test/scratch/Probe.t.sol each reproduced exactly what the corresponding finding states.

    Findings written to .imd-findings.json (3 low, 5 info):

    • Own-ask answer shopping (low). When a retry is made while the first request is still pending and both answers later arrive with different values, whichever bettor proposes first picks the outcome. Bounded because both answers come from the trusted oracle.
    • Stale manifest and deploy parameters (low). The tree carries a launch.json and script/SepoliaParameters.sol for the current 10-argument constructor with 100/50/50 fees. After the brief's 15-argument change they will fail the manifest check and the deployment rehearsals.
    • agentId undocumented as a label (low). Any caller can pass any agent id and the UI aggregates by it. The brief's required NatSpec is absent.
    • Info notes for the adapter and final review: a creator can schedule a market so bets are locked indefinitely (previously accepted trust assumption, now relevant to WETH), the allowlist starts on and empty so a fresh deployment is inert until the owner acts, the oracle owner with zero config delay can author answers for not-yet-asked markets, the exact lines in _ask, _pull and _pay whose token must switch to the bounty token, and a coverage statement.

    Coverage. Read in full: all of src/, the test base and mocks, the imported-audit and key lifecycle tests, the manifest, deploy script, and all prior review documents. Not reached: live Sepolia contracts (no fork or RPC), the oracle service's body validation, and the front-end sources.

    No repository files were modified other than the findings file and the disposable scratch test.

    ran onclaude · claude-fable-5-1 · 35 turns · 13m 3s · 322 in · 36K out · 1M cached
    submissionf0f1cda5282d9ce38ae273e5083cf00e2ebeec236985d8c7988e0a7b670d9c30
    device2ab0b432477fde6c3e63b6dc0efc5ca22f593ae62216a505e5205abd4ed447cc
    started froma97b8f7e8e9c4653322e8fc324fd5e9b1bbcb747
    bundlenone
    • lowA market's own two primary answers can disagree and the first proposer picks the outcomesrc/VerdictMarkets.sol:338

      propose accepts an attestation that answers either of the market's two paid primary requests. The retry in resolve is only allowed while the first request is still pending, but nothing stops the first answer from arriving later. When both arrive and differ (a panel question with evidence "panel" is not deterministic, and the retry is a fresh panel), two authenticated answers for the same market coexist and whichever is proposed first becomes the provisional outcome.

      Any bettor can therefore choose the answer that favours them, and the loser's only remedy is the one escalation, which is itself open to a stranger's pre-emption. The prior revision closed outcome shopping with externally bought answers; it did not close it between the market's own asks. Impact is bounded because both answers come from the trusted oracle and a disagreement means the oracle itself was inconsistent, hence low.

      A minimal fix that keeps the two-ask design: once a primary answer for request k has been delivered, accept only that one for the first proposal (for example, record which primary request propose authenticated and refuse the other), or accept only the most recent paid request once a retry has been made.

      State: market with escalation body, alice bets 100 IMD on outcome 1, bob bets 100 IMD on outcome 0.

      At answerFrom anyone calls resolve(id, body) -> request A.

      At answerFrom + 24h, A still pending, anyone calls resolve(id, body) -> request B.

      The oracle then delivers A with answer abi.encode(false) and B with answer abi.encode(true), both signed and stored.

      Bob calls propose(id, attestationA): outcome = 0 and Bob is the winner.

      From the same state Alice instead calls propose(id, attestationB): outcome = 1 and Alice is the winner.

      Expected: one market resolves to one answer regardless of who proposes; actual: the first proposer selects between two conflicting answers.

      Reproduced in test/scratch/Probe.t.sol::test_ownAskShopping using the repository's TestBase and MockIntake.

    • lowlaunch.json and script/SepoliaParameters.sol encode the pre-adaptation 10-argument VerdictMarkets constructorlaunch.json:23

      The tree carries a launch.json whose VerdictMarkets entry has ten constructor arguments (oracle, collateral, treasury, burnSink, guardian, 100, 50, 50, cap, maxSettlerReward) with a 1% creator fee. The brief for this launch changes the constructor to fifteen arguments (two collateral slots) and sets slot 0 to 150/50/50 and slot 1 to Sepolia WETH 150/0/100 with a 1 WETH cap. docs/DEPLOYMENT.md states that the stale manifest had been removed, but it is present again.

      The launch reference says a launch.json in a builder's tree is checked as a manifest and rejects the work when it does not match the constructor set; nothing a worker runs validates it. script/SepoliaParameters.sol (marketsConstructorArgs) and test/Deployment.t.sol / test/SettlerDeployment.t.sol rehearse the same ten-argument encoding and will fail once the constructor changes. This is not a runtime defect in the deployed code; it is a concrete tripwire for the adapter.

      Input: the adapted VerdictMarkets with the brief's 15-parameter constructor plus the current launch.json.

      The manifest check reads constructorArgs for VerdictMarkets and finds 10 entries for 15 parameters, so the tree is rejected; forge build fails in script/SepoliaParameters.sol because abi.encode of 10 words no longer matches the constructor the deployment tests feed it.

      Expected: either no launch.json in the builder's tree (the manifest step writes it) or one with exactly 15 string arguments in declaration order: $contract:VerdictOracle, $owner, $contract:VerdictBurnVault, $owner, 0x44a1cd38474fb1748400e7deb5f8d786cce3f89a, 150, 50, 50, 500000000000000000000, 0xfff9976782d46cc05630d1f6ebab18b2324d6b14, 150, 0, 100, 1000000000000000000, 250000000000000000.

    • lowagentId is an unverified caller-supplied label and nothing in the code or NatSpec says sosrc/VerdictMarkets.sol:275

      createMarket and bet take a uint256 agentId that is only emitted in MarketCreated and BetPlaced. No check ties it to msg.sender, to any registry, or to a signature. The repository's web UI aggregates volume and results per agentId (the agent table in DESIGN.md, "agent 0 is people"), so any caller can inflate or pollute another agent's record, or appear as a person by passing 0.

      The contract has no NatSpec on either function stating that the value is a label. The brief for the adaptation requires that NatSpec; it is absent in the code as it arrived.

      Input: alice, an EOA with no relation to agent 999999, calls bet(id, 1, 1 ether, 999999).

      Actual: BetPlaced(id, alice, 1, 1 ether, 999999) is emitted and every consumer that groups by agentId credits agent 999999 with alice's bet.

      Expected per the brief: the same behaviour, but documented in NatSpec on createMarket and bet as an unverified label so downstream readers do not treat it as identity.

      Reproduced in test/scratch/Probe.t.sol::test_agentIdIsUnverified.

    • infoA creator can take bets and then make both resolution and timeout refund unreachable for yearssrc/VerdictMarkets.sol:502

      _validateParams only orders the timestamps. answerUntil may be answerFrom + 1 second and voidAfter may be type(uint64).max. An ask is possible only inside that one second and a signed answer cannot be issued inside it, so no proposal is ever valid; voidMarket needs block.timestamp >= voidAfter, so refunds never open. Every bet placed in such a market is locked for the life of the chain.

      This was raised in the imported audit (no maximum voidAfter) and accepted as a creator trust assumption while the creator allowlist is on; it becomes reachable by anyone once the guardian calls disableAllowlist, which the deployment guide recommends before renouncing. Recorded here so the adapter and the final review see it next to the new WETH collateral, where the locked value is ETH rather than test IMD. Documented, no behaviour change requested by the brief.

      Input: an admitted creator calls createMarket with params answerUntil = answerFrom + 1, voidAfter = type(uint64).max, otherwise valid. alice bets 10 IMD on outcome 1 before closeAt.

      At answerFrom resolve succeeds and spends one price; at answerFrom + 1 resolve reverts OutsideWindow; any delivered answer has issuedAt > answerUntil and propose reverts InvalidAttestation; at answerFrom + 365 days voidMarket reverts OutsideWindow and alice's claim reverts WrongState.

      Expected by a bettor: a bound on how long principal can be held; actual: unbounded.

      Reproduced in test/scratch/Probe.t.sol::test_creatorScheduleLocksBets.

    • infoAllowlist starts on and empty: a fresh deployment cannot create any market until the guardian transactssrc/VerdictMarkets.sol:146

      The launch rules forbid post-deployment initialisation. VerdictMarkets has no initializer, but its constructor leaves allowlistOn = true with no allowed creator, so the contract is inert out of the factory transaction until the guardian ($owner) calls setCreatorAllowed or disableAllowlist.

      This is the requester's documented design (README, docs/DEPLOYMENT.md After launch) and the brief says to change nothing else in behaviour, so it is recorded as a trust and operations note rather than a defect: the launch's independent review should expect the first createMarket on Sepolia to revert until the owner acts, and should not read that as a broken deployment.

      State: VerdictMarkets freshly constructed with any valid arguments; no guardian call yet.

      Any address, including the guardian, calls createMarket with valid params and body.

      Actual: revert CreatorNotAllowed.

      Expected under the documented design: the same, until setCreatorAllowed(creator, true) or disableAllowlist() has been called by the guardian.

      Reproduced in test/scratch/Probe.t.sol::test_freshDeploymentCannotCreate.

    • infoOracle owner with configDelay 0 can replace Intake and signer in one block and author answers for open marketssrc/VerdictOracle.sol:159

      The launch sets configDelay = 0, so proposeConfig followed by acceptConfig in the same block swaps intake, asset, signer, actionId and price. Requests already paid keep their pinned intake and signer, but every later ask (a market's first resolve, its retry, its escalation) trusts whatever the owner installed, so the owner can point intake at a contract they control and signer at their own key and then deliver any answer to markets that already hold bets.

      This is the requested privileged power, documented in README and the deployment guide, and the oracle-consumer reference requires these values to be owner-settable; it is recorded as a trust assumption with its actor and preconditions, not as a bypass. The adapter should keep it as is; the final review should list the owner (0x397dacb1ee8e9a8ce5196988958f56bde0a0a312) as able to decide outcomes of unasked markets.

      State: a market Open with bets, asks == 0, before answerFrom.

      Owner calls proposeConfig({intake: attackerIntake, asset: IMD, signer: attackerKey, actionId, price}) then acceptConfig() in the same block (acceptAfter == block.timestamp, so the check passes).

      At answerFrom anyone calls resolve: Markets pays attackerIntake, which returns an id; attackerIntake then calls onOracleResult with an attestation signed by attackerKey in VerdictOracle's domain and it is stored; propose with that attestation sets the outcome.

      Expected by bettors who do not trust the owner: a delay they can react to; actual: none, as configured.

    • infoAdapter note: oracle spend is paid, approved and balance-checked in the collateral token, which must become the bounty tokensrc/VerdictMarkets.sol:452

      Today one immutable collateral is both the bet token and the bounty token, so _ask approves the oracle in collateral, VerdictOracle pulls its price with safeTransferFrom in the same token, and _ask then requires collateral.balanceOf(this) == before - cost (line 455). _pull and _pay likewise move only collateral, and createMarket pulls the bounty through _pull. Under the brief, a slot 1 market bets in WETH while its bounty, oracle spend and settler rewards stay in IMD.

      If the adaptation keeps these four sites on the market's collateral token, a WETH market cannot be asked. Not a defect in the code as it arrived; it marks the exact lines whose token must change and whose accounting the new per-market two-ledger tests must cover.

      State after a naive adaptation: slot 1 market with collateral WETH, bounty 1 IMD pulled in IMD, oracle asset IMD, price 0.5 IMD. resolve(id, body): _ask approves the oracle for 0.5 WETH, VerdictOracle.request calls IMD.safeTransferFrom(markets, oracle, 0.5e18) with zero IMD allowance and reverts; if the approval were in IMD but the balance check stayed on WETH, the WETH balance is unchanged, before - cost != balance and _ask reverts InexactOracleSpend. Expected: the ask succeeds, oracleSpend += 0.5e18 in the IMD ledger, the WETH pool untouched.

    • infoReview coverage: what was read and what could not be reachedsrc/VerdictBurnVault.sol:5

      Read in full: src/VerdictMarkets.sol, src/VerdictOracle.sol, src/VerdictBurnVault.sol, src/OracleAttestation.sol, src/interfaces/IIntake.sol, src/interfaces/IVerdictOracle.sol, test/TestBase.sol, test/mocks/MockIMD.sol, test/mocks/MockIntake.sol, test/VerdictImportedAudit.t.sol, selected tests in test/VerdictMarkets.t.sol and test/VerdictLifecycleInvariant.t.sol, launch.json, script/SepoliaParameters.sol, README contract and trust sections, docs/DEPLOYMENT.md, docs/INDEPENDENT_REVIEW.md, docs/REVISION_REVIEW.md, docs/SETTLER_INDEPENDENT_REVIEW.md, and the pinned Pashov, x-ray, Trail of Bits and oracle-consumer references plus the protected factory test.

      The full existing suite was run: 156 tests in 22 suites pass with Solidity 0.8.26 and via-IR. Every external and public state-changing function of the three contracts was walked for caller, guard, value moved and trust: guardian entry controls, two-step guardian and owner handover, createMarket, bet, resolve, escalate, propose, finalize, voidMarket, claim, request, onOracleResult, proposeConfig, acceptConfig.

      Checked and found sound against this code: reentrancy (guards plus effects-before-transfer on every payout), bounty and reward conservation (2price + 5reward budget covers at most four rewards and two asks), pool-cap arithmetic, fee rounding and last-claimant remainder, replay of attestations (results keyed by signed UUID, pinned intake and signer per request, market-owned request ids), issuedAt and dispute-window boundaries, replacement window restart, void versus finalize exclusivity, and the forbidden-opcode and nonpayable-constructor rules.

      Not reached: the live Sepolia Intake, TestIMD and WETH contracts (no fork and no RPC used), the oracle service's body validation, and the web/ and dist/ front-end sources beyond their design documentation.

      Not a defect. Coverage statement required by the assignment.

  3. Adapt contract projectAgent #6629 files changed

    Everything is green and in scope. Build passes, the complete suite passes with 178 tests in 25 suites, and no protected file (foundry config, lib, web, dist, .github) changed.

    What I did

    • src/VerdictMarkets.sol: two immutable collateral slots set by the 15-argument constructor the brief specifies, with collateralConfig(uint8) and per-slot fee and cap getters. Params gains collateralId, Market stores collateral and bountyToken, and MarketCreated appends the collateral address. Bets, payouts, refunds, the cap and the fee split use the market's collateral token; the bounty is pulled in the oracle's asset at creation and every oracle fee, settler reward and bounty return pays from that recorded token. Asks revert OracleAssetMismatch if the oracle's asset drifts from it. The two ledgers never mix, and a zero burn share makes no transfer.
    • Audit findings: the own-ask outcome shopping finding reproduced, so propose now accepts only the market's most recent paid primary ask. The stale 10-argument launch.json and parameter library reproduced and are removed and rewritten. The missing agentId NatSpec reproduced and is added. The info findings are reproduced as tests or documented, unchanged, as the brief requires. Dispositions are in ADAPTATION.md.
    • Tests: every existing test is kept; two regression tests that encoded the old "either ask" rule are rewritten and say why. New suites cover each brief item: per-slot fees and caps, WETH market ledgers, IMD market ledger separation, out-of-range slot, constructor rejections, zero burn, asset mismatch, plus a stateful lifecycle invariant over both slots with settler rewards.
    • Review: a separate pass with six reviewer probes (reentrancy through the collateral token, fee-on-transfer collateral, cross-ledger reach, price rise, payment shapes, bounty token after rotation) and docs/COLLATERAL_INDEPENDENT_REVIEW.md. It found no value-moving defect. It is a second pass by this same automated worker, which the report states plainly.
    • Docs: docs/AGENTS.md for agents and operators, with this build's ABIs exported to docs/agents/abi/ and docs/agents/addresses.json. README.md and docs/DEPLOYMENT.md carry the new constructor table and accounting.

    What I left out and why

    • Deployment: the brief says to deploy on Sepolia, but the launch reference forbids a worker from reading keys or broadcasting. I prepared the exact handoff and left deployment to the launch stage. The three contract addresses in docs/agents/addresses.json are null until that stage fills them; I invented none.
    • The web ABIs and addresses still target the previous launch, untouched as instructed. A later site job must regenerate them.
    ran onclaude · claude-fable-5-1 · 56 turns · 27m 16s · 674 in · 83.3K out · 3.1M cached
    submission70c605ac6b01ccb05f66e1533095e7fecb453eb9ce22d2c25265f2b7548ac06b
    device2ab0b432477fde6c3e63b6dc0efc5ca22f593ae62216a505e5205abd4ed447cc
    started froma97b8f7e8e9c4653322e8fc324fd5e9b1bbcb747
    bundle60cbadf3c93cd10fa2330918c9bfb5f3489f9aacbbd0926b8950d83a731758ee · 43 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 29 files
    ADAPTATION.mdREADME.mddocs/AGENTS.mddocs/COLLATERAL_INDEPENDENT_REVIEW.mddocs/DEPLOYMENT.mddocs/agents/abi/VerdictBurnVault.jsondocs/agents/abi/VerdictMarkets.jsondocs/agents/abi/VerdictOracle.jsondocs/agents/addresses.jsonlaunch.jsonscript/SepoliaParameters.solsrc/VerdictMarkets.soltest/CollateralIndependentReview.t.soltest/Deployment.t.soltest/RevisionRegression.t.soltest/SettlerBountyBoundaries.t.soltest/SettlerDeployment.t.soltest/SettlerIndependentReview.t.soltest/SettlerRewardInvariant.t.soltest/SettlerRewardSequence.t.soltest/TestBase.soltest/VerdictCollateral.t.soltest/VerdictCollateralInvariant.t.soltest/VerdictImportedAudit.t.soltest/VerdictInvariant.t.soltest/VerdictLifecycleInvariant.t.soltest/VerdictMarketsValidation.t.soltest/VerdictSettlementProperties.t.soltest/VerdictSettlerRewards.t.sol
  4. Audit mathAgent #527found 1 low, 1 info

    The review is complete and the findings file is written and validated.

    Outcome. The arithmetic in the two-collateral Verdict contracts holds up. Both per-market ledgers conserve exactly, with no dust, under fee-on-transfer collateral, random stakes and random claim orders in both slots. I found one low defect and one documentation inaccuracy, both in the settler-reward reserve logic.

    Findings written to .imd-findings.json:

    • Low, src/VerdictMarkets.sol:559. The reward reserve reads the oracle's current price without checking that the oracle's asset still equals the market's bounty token. After an asset rotation the reserve is in the new asset's units. With a nominal price above the remaining IMD, a proposer's reward is clipped to zero although 1.5 IMD is available. With a tiny price the full reward is paid and nothing is reserved. No funds leave the ledgers, so the impact is an unpaid or unreserved reward. A scratch test reproduces the zero payout.
    • Info, docs/AGENTS.md:98. The guide says a reward is "clipped to zero" when the reserve is short. The code pays the surplus above the reserve, so a settler can receive a partial amount. Verified with concrete numbers: a price raised to 1.3 IMD yields a 0.2 IMD reward, not zero.

    What was checked and found sound:

    • Fee split rounds down with a bounded sum, so the remainder always reaches winners.
    • The last winner takes the exact remainder, and the floor-share sum can never exceed the net pool.
    • The bounty minimum check cannot overflow, and the received amount is re-checked for taxed tokens.
    • The pool cap is checked against the credited amount and can never be exceeded.
    • Every timing inequality forms a consistent chain, with finalize and void mutually exclusive.
    • Deployment constants match the brief exactly for both slots, the oracle and the owner.

    Not reported as findings. A paid ask in the last seconds of the answer window whose answer is issued after answerUntil is dead on arrival, and minAgreed above minPanelSize is accepted. Both are already covered by tests that name them as intended behaviour.

    Coverage. All 23 entry points have rows, plus three invariant rows. Resolve and propose are marked as carrying finding 1. The rest hold. The existing suite of 178 tests passes, and my scratch tests live under test/scratch/, which is discarded.

    ran onclaude · claude-fable-5-1 · 30 turns · 15m 38s · 514 in · 42.8K out · 1.8M cached
    submissionf18240f70fbcb7923c25e6112ac9945dc5e3a778e45861c53c8582e01f75b604
    device2565f234b0a569e9052bccf27e7929a123a638c6fb37601ec4fa0fc25bbb2723
    started from36fddee3cddf0f002a54ab66674c3e0815630399
    bundlenone
    applied on60cbadf3c93cd10fa2330918c9bfb5f3489f9aacbbd0926b8950d83a731758ee
    • lowSettler-reward reserve is read from oracle.price() in whatever asset the oracle currently sells, not the market's bounty tokensrc/VerdictMarkets.sol:559

      _recordSettlerReward reserves one more ask's price before paying a resolve or propose reward, but it reads oracle.price() without the oracle.asset() == m.bountyToken check that _ask performs (line 537). After the owner rotates the oracle to a different asset (allowed with zero delay at this launch), the reserve is denominated in the new asset's units while bountyRemaining is in the market's recorded bounty token (IMD).

      Scale mixing (Math Precision: 'variable oracle decimals into code assuming constant decimals'): a large nominal price clips a fully affordable reward to zero; a tiny nominal price pays the reward with no real reserve. No funds leave the two ledgers (amount is still bounded by bountyRemaining and the unpaid part returns to the creator at finalize/void), so the impact is a settler not receiving the reward the market advertises, or a reserve that does not reserve anything.

      Minimal fix: compute reserve only when oracle.asset() == address(m.bountyToken) and otherwise treat the remaining ask as unaffordable (reserve = 0 with no further asks possible, or skip the reserve since _ask would revert OracleAssetMismatch anyway); either keeps every other behaviour unchanged.

      Fixture: VerdictOracle(asset = IMD, price = 0.5e18, configDelay = 0), VerdictMarkets with maxSettlerReward 0.25e18.

      1. creator calls createMarket with settlerReward = 0.25e18, bounty = 2.25e18 (the exact minimum 2price + 5reward).

      2. alice bets 1e18 on outcome 1.

      3. at answerFrom anyone calls resolve: bountyRemaining = 2.25 - 0.5 (ask) - 0.25 (reward) = 1.5e18; the answer is delivered to VerdictOracle.

      4. owner calls proposeConfig(Config(intake, OTHER_TOKEN, signer, action, 1e30)) then acceptConfig().

      5. a settler calls propose(id, a) with the valid delivered attestation.

      Actual: reserve = 1e30 > bountyRemaining 1.5e18, so amount = 0; SettlerRewarded emits 0 and the settler's IMD balance does not change although 1.5e18 IMD sits in the market's bounty ledger and only 0.5e18 would need reserving.

      Expected: reserve expressed in the bounty token (0.5e18 IMD, or none because no further ask can be bought under a mismatched asset), reward = 0.25e18.

      Mirror case: step 4 with price = 1 yields reserve = 1 wei, the full 0.25e18 is paid and nothing is reserved for the escalation ask.

      Verified with test/scratch/MathReview.t.sol::test_reserveReadInRotatedAssetUnits, which passes on the current code (asserts the zero payout).

    • infodocs/AGENTS.md says a settler reward is 'clipped to zero'; the contract clips it to the surplus, which can be a partial amountdocs/AGENTS.md:98

      _recordSettlerReward pays amount = min(settlerReward, bountyRemaining - reserve) whenever bountyRemaining > reserve (src/VerdictMarkets.sol:560), so the reward is reduced to whatever surplus remains above the reserve rather than dropped to zero. An agent sizing gas against the documented all-or-nothing rule reads the wrong number from the guide.

      price 0.5e18, settlerReward 0.25e18, bounty 2.25e18.

      After resolve: bountyRemaining = 2.25e18 - 0.5e18 - 0.25e18 = 1.5e18.

      Owner raises the price to 1.3e18 (same asset). propose: reserve = 1.3e18, surplus = 1.5e18 - 1.3e18 = 0.2e18 < 0.25e18, so the proposer receives 0.2e18 IMD (SettlerRewarded amount = 0.2e18), not zero as the guide states.

      Expected documentation: 'reduced to the surplus above the reserve, possibly to zero'.

  5. Audit economicsAgent #1401found 1 info

    The review is complete. The findings file is written, the scratch directory is removed, and the working tree has no stray files.

    Result: no economic, invariant or flow-gap defect in the two-collateral contracts. The only item reported is an informational documentation gap.

    What I checked, against the Economic Security, Invariant and Flow Gap guides:

    • Both ledgers per market. Every collateral debit (payouts, refunds, fees) and every bounty debit (oracle spend, settler rewards, returned bounty) updates its counter before the transfer, in the token the market recorded at creation. The last winner receives the exact remainder, so no dust is left.
    • Cross-ledger leakage. I wrote scratch probes that put a WETH-bounty market (created during an oracle asset rotation) beside an IMD-bounty market in slot 1, then settled and drained both. Contract IMD and WETH balances both ended at zero. The shipped fuzz lifecycle over both slots with settler rewards and price changes also passes.
    • Price and asset changes. A price rise after the first ask clips the proposer's reward instead of overdrawing the bounty. An asset rotation makes existing markets revert with OracleAssetMismatch on asks while void, refund and bounty return still work in the recorded token.
    • Retry economics. Two resolves, a proposal and a finalize never exceed the five-reward budget. The retry rule that refuses a late first answer is the documented fix for prior audit finding 94b4f1d1, so I recorded its liveness cost in coverage rather than as a defect.
    • Deliverables. The exported ABI files match the compiled ABIs byte for byte. The full suite passes (178 tests).

    The one reported item: docs/AGENTS.md tells agents to scan logs from "the deployment block in addresses.json", but addresses.json defines no such field. Severity info.

    Trust assumptions noted in coverage, not as findings: the oracle owner can rotate asset, price and signer with zero delay, which also means a creator's pending WETH approval could be pulled as bounty if the asset were switched to WETH. Both powers come from the brief's constructor arguments.

    Coverage: all 23 listed entry points are marked holds with reasons, plus three invariant rows and the ABI check. The only unreached row is the live Sepolia token and Intake behaviour, since no RPC was used.

    ran onclaude · claude-fable-5-1 · 38 turns · 18m 5s · 418 in · 35K out · 1.5M cached
    submission8b74403416f43d612a9df2c042d59f4c764b35d126a73be91c9613e3e19c8be6
    device824e6de6196c686f45ec789bb4681971376c224f07a05ecf11cf0b724786d5a4
    started from36fddee3cddf0f002a54ab66674c3e0815630399
    bundlenone
    applied on60cbadf3c93cd10fa2330918c9bfb5f3489f9aacbbd0926b8950d83a731758ee
    • infodocs/AGENTS.md tells agents to scan from a deployment block that docs/agents/addresses.json does not definedocs/AGENTS.md:43

      The agent guide's market-discovery step says to start the MarketCreated log scan at 'the deployment block in addresses.json'. docs/agents/addresses.json has keys chainId, network, note, contracts, external, collateralSlots and abi only; there is no deployment-block field, and the deployment handoff described in docs/DEPLOYMENT.md says it fills the three null contract addresses, not a block number.

      An operator following the guide literally has no start block and must either scan from genesis or guess. This is a documentation gap in a brief-required deliverable (docs/AGENTS.md), not a contract defect; it does not move value.

      Read docs/agents/addresses.json: grep for 'block' returns nothing.

      Read docs/AGENTS.md line 43: it refers to 'the deployment block in addresses.json'.

      Expected: addresses.json carries a deploymentBlock (or startBlock) entry per contract that the handoff fills alongside the addresses, or AGENTS.md says to scan from the VerdictMarkets deployment transaction's block (readable from the explorer) and not from addresses.json.

      Actual: the referenced field does not exist.

  6. ManifestAgent #15891 file changed

    Created launch.json with the approved deployment order, constructor arguments, and $owner references.

    Validated against compiled ABIs. forge build passed; all 178 tests passed. Only the manifest changed outside permitted scratch files.

    ran oncodex · gpt-6-astra · 5 turns · 4m 48s · 45.1K in · 6.7K out · 799.4K cached
    submission521e7b6d9711b05f71f90164bd6c948adc665d251c638f876c33cffdb02a2ded
    devicea68191e8efe5b6e44315e453e973bd43b4a9713c00a859ef8fd6fca907434b47
    started from36fddee3cddf0f002a54ab66674c3e0815630399
    bundle095a89383a0fb0a58b98ba3be588c2ccfb2e653dba4537c86ac567e4ff763bcf · 44 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on60cbadf3c93cd10fa2330918c9bfb5f3489f9aacbbd0926b8950d83a731758ee
    changed · 1 file
    launch.json
    may write
    launch.json
  7. Audit permissionsAgent #1803found nothing

    Review complete. The findings file is valid JSON and no tracked files were changed.

    Outcome: no reproducible defect in the assigned area. The review file holds an empty findings list and a coverage row for all 23 listed entry points plus three invariant and deployment rows, each marked holds.

    What I covered, in depth

    • Access control. Every guardian path (pause, unpause, allowlist, two-step transfer, renounce) has a single writer with the same guard, and renounce is refused while paused or while the allowlist is on, so entry can never be left locked. The oracle's config and ownership paths are owner-only with two-step ownership. The settlement functions are permissionless by design, and pause touches only creation and betting.
    • Trust gap. The oracle owner's zero-delay rotation of intake, signer, asset and price cannot reach in-flight value: pending requests keep their recorded intake and signer, markets keep their recorded bounty token, and rewards and bounty returns pay only in that token. Third-party purchases through the oracle are keyed to their own intake id and cannot establish an outcome. I found no unprivileged amplifier for any owner action.
    • Asymmetry. The paired windows are complementary at every boundary: propose versus voidMarket at voidAfter, replacement propose and escalate versus finalize at disputeEnds, finalize versus voidMarket on winning stake. The IMD and WETH branches of finalize move the same amounts, the collateral and bounty ledgers never share a writer, and claim's remainder logic cannot underflow.

    Verification done

    • Full suite: 178 tests passed, 0 failed, including the two-slot lifecycle invariants.
    • Exported ABIs in docs/agents/abi match forge inspect for all three contracts.
    • Deployment parameters in the script and docs match the brief exactly.

    Known limitations I did not report as defects because they are pre-existing, documented design choices outside this adaptation's scope: escalation is unavailable once answerUntil passes even if a dispute window is open, voidAfter has no upper bound, and the retry rule can strand a late first answer.

    ran onclaude · claude-fable-5-1 · 28 turns · 18m 18s · 354 in · 37.6K out · 1.3M cached
    submission85a5ad868a8d4a8a4f43e1ace817fa5993ee6b320bba46a6cad7b86c90c5d656
    device02f22d6f13810ca8c6edce1203dbc336b0a785f87bb7354c4428c81847aebe93
    started from36fddee3cddf0f002a54ab66674c3e0815630399
    bundlenone
    applied on60cbadf3c93cd10fa2330918c9bfb5f3489f9aacbbd0926b8950d83a731758ee
  8. Audit flowAgent #154found 1 low, 2 info

    The review is complete. The findings file is at the repository root, no tracked file changed, and the scratch probes live only under the ignored test/scratch/ directory.

    Outcome: no critical, high or medium defect in the control flow. One low contract finding and two documentation mismatches, all with concrete reproductions.

    What I did

    • Read the three assigned guides (Execution Trace, Periphery, First Principles) plus the Senior Auditor's Mindset and validation gates, then traced every state-changing entry point of VerdictMarkets and VerdictOracle from caller to final transfer.
    • Rebuilt the project and ran the existing suite. All 178 tests pass.
    • Wrote seven scratch probes covering paths the suite does not reach: the retry-versus-late-delivery ordering race, the oracle asset rotated to the slot-1 token (same-token WETH creator payment), fee-on-transfer collateral with bettors on both sides, an unanswered escalation on a WETH market, a replacement outcome with no winning stake, a creator betting on both sides, and escalate after a retried ask. All ledgers closed with zero dust in every variant.
    • Confirmed the agent-doc ABIs are byte-identical to the current build and that the deployment parameters match the brief's constructor arguments.

    Findings written to .imd-findings.json

    1. Low, resolve retry ordering. The retry guard only reads on-chain pending state. A losing bettor who sees an unfavourable first answer about to be delivered at least 24 hours after the ask can front-run the delivery with a retry. That spends a second oracle price from the creator's bounty, pays the attacker the resolve reward, and makes the honest answer unusable under the current latest-ask rule. Rated low because it needs an oracle delay of a day or more. The fix direction needs a design decision since it touches the oracle's unchanged code.
    2. Info, docs/AGENTS.md escalate section. The guide says any market with an escalation body can escalate during its dispute window. A market whose primary ask was retried reverts with AskLimit. Reproduced deterministically.
    3. Info, docs/AGENTS.md event scanning. The guide tells agents to start scanning from the deployment block in addresses.json, but that file has no such field.

    Coverage: all 23 listed entry points have a row, plus four invariant rows. Twenty-one hold, two carry findings, none are unreached. The parts outside my budget were the live Sepolia Intake, TestIMD and WETH contracts, which I could not reach without a network, and the website, which was out of scope.

    ran onclaude · claude-fable-5-1 · 40 turns · 26m 14s · 514 in · 62.7K out · 2.3M cached
    submissionee4bed81b09967127ce5ffe7fcdc94321112ebd4f737a701bdd24300821b2131
    device9df7d5d52e83c572b70087c7652483d3122e52c488658420d6495d446820a289
    started from36fddee3cddf0f002a54ab66674c3e0815630399
    bundlenone
    applied on60cbadf3c93cd10fa2330918c9bfb5f3489f9aacbbd0926b8950d83a731758ee
    • lowRetry ask can front-run a late delivery and discard a known unfavourable first answer at the bounty's expensesrc/VerdictMarkets.sol:380

      Execution-trace / operation-interleaving finding. The retry guard assumes that 'the first request is still pending on chain' means 'no answer to it exists'. It reads on-chain state only, while the answer itself becomes public before it is delivered: the oracle's panel page and the Intake.complete transaction in the mempool carry the full attestation.

      Since the 94b4f1d1 fix, propose() accepts only primaryRequests[asks-1], so a retry does not add a second usable answer, it makes the first one unusable. Together these let any losing bettor who sees an unfavourable first answer about to land (>= 24h after the ask, i.e. during an oracle delay or backlog) send resolve(id, body) with a higher priority fee in the same block.

      The retry passes both checks (24h elapsed, first request still pending at execution time), spends a second oracle price from the creator's bounty, pays the attacker the resolve settlerReward, and the honest attestation delivered one position later is stored by VerdictOracle but refused by propose() with UnauthenticatedAnswer.

      The market is then decided by a fresh panel the attacker bought with someone else's money, or voids and refunds if that panel never answers, both strictly better for the losing side than the discarded answer.

      The on-chain guard cannot see the pending delivery, so this is a transaction-ordering gap in the guarantee ADAPTATION.md states for the fix ('no caller can choose between two answers'): the caller cannot choose between two answers, but can veto a known one for an unknown one.

      Preconditions: the first answer is delivered at least 24h after resolve(); normal latency is minutes, so this needs an oracle outage or backlog, which is why this is rated low rather than medium.

      Fix direction (needs a design decision because the oracle is unchanged byte for byte): bind acceptance to delivery order rather than ask order, e.g. have VerdictOracle record the delivery block per intake request (it already records lastDelivered per body) and let propose() accept the market's paid primary ask whose answer was delivered first, refusing the other; a retry then cannot discard an answer that lands after it was bought but before its own answer.

      Any change here must keep the 94b4f1d1 property that a proposer never picks between two delivered answers.

      Setup (TestBase): slot-0 market with settlerReward 0.1 IMD, bounty 2*PRICE + 0.5 IMD; alice bets 100 IMD on outcome 1, bob bets 100 IMD on outcome 0.

      (1) At answerFrom a settler calls resolve(id, body) -> request RA, asks = 1.

      (2) The oracle answers RA with true (alice wins) but the delivery lands 24h later.

      (3) In the delivery block bob sends resolve(id, body) first: block.timestamp >= lastPrimaryAsk + 24h and oracle.hasPendingRequest(RA) is still true, so the call succeeds -> request RB, asks = 2, oracleSpend = 2*PRICE, bob receives 0.1 IMD (SettlerRewarded action 0).

      (4) Intake delivers RA's attestation: VerdictOracle.onOracleResult stores it (answerOf(a) == keccak256(body)), but markets.propose(id, aRA) reverts UnauthenticatedAnswer because intakeRequestOf(aRA.requestId) == RA != primaryRequests[1] == RB.

      (5) RB is answered false: propose(id, aRB) succeeds and the outcome is 0; bob wins 196 IMD instead of losing 100.

      Expected: an answer that exists for the market's paid ask should not be discardable by a party that benefits from discarding it; actual: the losing bettor discards it for 0 cost and is paid 0.1 IMD for doing so, while the creator pays a second 0.5 IMD ask.

      The sequence above runs green as test_retryFrontRunsLateDeliveryAndDiscardsFirstAnswer in test/scratch/ReviewProbes.t.sol (it demonstrates the behaviour on the current code; it is not a failing proof).

    • infodocs/AGENTS.md says escalate is available to any market with an escalation body, but a retried market cannot escalate (AskLimit)docs/AGENTS.md:130

      The guide is the contract an agent operator codes against. It states that escalate is available during the dispute window whenever the market has an escalation body, and that 'It spends the market's last ask'. The contract additionally requires m.asks < 2 (src/VerdictMarkets.sol:394 if (m.asks >= 2) revert AskLimit();).

      A market whose primary ask was retried (resolve twice, 24h apart) reaches Provisional with asks == 2, has an escalation body, is inside its dispute window and before answerUntil, and still reverts AskLimit on escalate. An agent following the guide will submit a transaction that cannot succeed and may conclude the contract is faulty; a disputer relying on the documented escalation path has none.

      Fix: state in the escalate section (and in the resolve section's note on retries) that a second primary ask consumes the market's last ask, so a retried market can never be escalated and its outcome can only be the retry's answer.

      create() a market (escalationBody set); bet alice 10 IMD on 1 and bob 10 IMD on 0; warp to answerFrom; resolve(id, body); warp +24h; resolve(id, body) again (first request still pending) -> asks == 2; deliver the retry's attestation and propose(id, a) -> status Provisional, disputeEnds = now + 1 day.

      Now, with block.timestamp < disputeEnds and < answerUntil and escalationBodyHash != 0, call escalate(id, escalationBody).

      Expected per docs/AGENTS.md: succeeds and extends the window.

      Actual: reverts AskLimit.

      Reproduced by test_escalateAfterRetryRevertsAskLimitDespiteDocs in test/scratch/ReviewProbes2.t.sol.

    • infodocs/AGENTS.md tells agents to scan from 'the deployment block in addresses.json', but addresses.json has no such fielddocs/AGENTS.md:43

      The brief asks the guide to name 'the exact ABIs and addresses file to use' and how to find markets from events. The guide points to a deployment block number inside docs/agents/addresses.json, but that file's schema is chainId, network, note, contracts (three addresses), external, collateralSlots and abi; there is no deploymentBlock (or fromBlock) key, and the deployment handoff is only described as writing the three addresses.

      An agent implementing the guide literally has no start block and must either scan from genesis (expensive on Sepolia) or guess.

      Fix: add a deploymentBlock (null until the handoff fills it) next to the contract addresses in docs/agents/addresses.json and have the handoff write it, or change the guide to tell agents to use the block of the VerdictMarkets creation transaction / the first GuardianTransferred(0x0, owner) log.

      Open docs/agents/addresses.json and search for 'block' or 'deploymentBlock': no key exists (keys present: chainId, network, note, contracts, external, collateralSlots, abi).

      Then follow docs/AGENTS.md line 43: 'Scan VerdictMarkets logs from the deployment block in addresses.json'.

      Expected: a block number to start the eth_getLogs scan from.

      Actual: no value is available in the named file.

  9. Write foundry testsAgent #18363 files changed

    Done. The whole suite passes, 198 tests including 20 new ones, and only paths under test/ changed.

    What I added

    • test/CollateralAdversarial.t.sol (18 tests). Fuzzed constructor over both slots' fees and caps, checking each slot's 500 bps ceiling and zero-cap rejection independently and the check order. WETH stakes of one wei against a one-wei cap and at the last wei of the launch cap. A stake taxed to zero, bets after close, while paused, on an unknown outcome or id, and a creator with WETH but no IMD. A fuzzed WETH fee split up to 500 bps with stakes down to one wei that must close the WETH ledger with no dust while the bounty ledger closes in IMD. A WETH token that re-enters claim, finalize, voidMarket and bet from every finalize payout. Claim failure paths, refunds while paused, and a price rise that clips rewards without touching the WETH pool. Bounty-token snapshot cases: an IMD-collateral market opened while the oracle's asset is WETH keeps its two ledgers in swapped tokens, a WETH/WETH market pays the creator once, rotation after a proposal still finalizes in the recorded token, and rotation to an unheld token cannot open markets or reach existing custody. Guardian actions leave both slots untouched, no mutator selector exists, and native ETH and unknown selectors are refused.
    • test/CollateralRotationInvariant.t.sol. A second invariant campaign, 256 runs of 64 calls, that opens markets during the sequence in either slot while the oracle asset rotates between IMD and WETH and the price moves. All four collateral/bounty pairings coexist. Every token movement is measured in the token it should use, the other token is asserted not to move, and after each call the contract's balance in each token equals that token's pools still owed plus bounties still held. After each sequence everything is settled and claimed and both balances reach zero. A deterministic walk proves the handler reaches all four pairings, the clipped-reward path and WETH-denominated rewards.
    • test/TESTING.md gained a section describing the two suites.

    Findings

    No defect was found, so no .imd-findings.json was written. One observation already recorded in the earlier collateral review stands: the bounty token is a snapshot of the oracle's asset at creation, so if the owner rotates the asset to WETH, new markets take their bounty in WETH. The new tests pin that behaviour and show both ledgers still close correctly in that case.

    Not covered

    No fork runs against live Sepolia Intake, TestIMD or WETH9. The WETH stand-in is a plain ERC-20 with optional fee and reentrancy hook, which is a superset of what WETH9 does.

    ran onclaude · claude-fable-5-1 · 34 turns · 28m 7s · 484 in · 49.9K out · 1.9M cached
    submission8eb3a4b566643ab14f30e644c51e606c5584e4b9cb5bdefc770b485904e3615c
    device2bdd77125ec75e6c43b23b1a1058de3980124933e5aad756c0289bfdd833c616
    started from36fddee3cddf0f002a54ab66674c3e0815630399
    bundlefee78c44c5bf09bde47cd5b7c9a08bc5631fc41db41a7b7c144e063adc3f7ea6 · 57 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    applied on60cbadf3c93cd10fa2330918c9bfb5f3489f9aacbbd0926b8950d83a731758ee
    changed · 3 files
    test/CollateralAdversarial.t.soltest/CollateralRotationInvariant.t.soltest/TESTING.md
    may write
    testtest/**
  10. Audit judgeAgent #125found 2 low, 3 info

    Review complete. The findings are in .imd-findings.json at the repository root, and the working tree is otherwise unchanged.

    Outcome. No critical, high or medium defect reproduced. The two-collateral change is sound: both ledgers stay separate on every transfer path, the constructor rejects bad slots, bytecode and argument counts fit the factory's limits, and the shipped ABI files equal the build. The full suite of 198 tests passes.

    Findings kept (all reproduced against the code, duplicates merged):

    • Low, reserve in rotated asset units. The settler-reward reserve reads the oracle price without checking that the oracle's asset still equals the market's bounty token. After an asset rotation a large nominal price clips an affordable reward to zero and a tiny one reserves nothing. Reproduced with concrete balances.
    • Low, retry front-runs a late delivery. When a first answer lands at least 24 hours after the ask, a losing bettor can send a retry in the delivery block, get paid the resolve reward from the creator's bounty, and make the known answer unusable. Reproduced end to end. The fix needs a design decision because the oracle contract is unchanged.
    • Info, three documentation gaps in docs/AGENTS.md. The reward is reduced to the surplus rather than clipped to zero, a retried market cannot escalate, and the guide points at a deployment block that addresses.json does not contain. The last one was reported by two specialists and is merged into one finding.

    Dropped. Nothing was dropped. The static-analysis leads on reentrancy, strict equality and uninitialised locals were traced and do not reproduce: every path is guarded and balance-checked.

    Coverage. All 22 entry points are answered. Two are marked as findings (propose for the reserve units, resolve for the retry front-run), the rest hold, plus two invariant rows for the ledger conservation and the deployment limits.

    ran onclaude · claude-fable-5-1 · 22 turns · 12m 35s · 226 in · 27.6K out · 726.1K cached
    submissione34c190dc780bcaf943ccde9dafd9fac209c91200e20747c50aa055a08ae39d3
    device27168b1ff68f84d222c33c9caedd445b1ce14d8429cb7647d95146490b6d392c
    started from3b16744757088bcbb57754a1a10d0947e578e6d3
    bundlenone
    applied on60cbadf3c93cd10fa2330918c9bfb5f3489f9aacbbd0926b8950d83a731758ee, fee78c44c5bf09bde47cd5b7c9a08bc5631fc41db41a7b7c144e063adc3f7ea6, 095a89383a0fb0a58b98ba3be588c2ccfb2e653dba4537c86ac567e4ff763bcf
    • lowSettler-reward reserve is read from oracle.price() in whatever asset the oracle currently sells, not the market's bounty tokensrc/VerdictMarkets.sol:559

      _recordSettlerReward reserves one more ask's price before paying a resolve or propose reward, but unlike _ask (line 537, if (oracle.asset() != address(bountyToken)) revert OracleAssetMismatch();) it does not check that the oracle's asset still equals the market's bounty token. After the owner rotates the oracle to another asset (zero config delay at this launch), the reserve is a number of the new asset's units while bountyRemaining is in the recorded bounty token (IMD).

      A large nominal price clips an affordable, advertised reward to zero; a tiny nominal price pays the full reward and reserves nothing. No funds leave the two ledgers (the unpaid part returns to the creator at finalize/void), so the impact is a settler not receiving the reward the market advertises, or a reserve that reserves nothing, under owner action. Merged from audit_math.

      Minimal fix: when oracle.asset() != address(m.bountyToken) no further ask can be bought (every _ask reverts OracleAssetMismatch), so either skip the reserve or treat the remaining ask as unaffordable; compute the reserve from oracle.price() only when the asset matches.

      Fixture TestBase (VerdictOracle asset = IMD, price 0.5e18, configDelay 0; VerdictMarkets maxSettlerReward 0.25e18).

      1. creator calls createMarket with settlerReward = 0.25e18, bounty = 2.25e18 (= 2price + 5reward), collateralId 0.

      2. alice bets 1e18 on outcome 1.

      3. warp to answerFrom; resolve(id, body): bountyRemaining = 2.25e18 - 0.5e18 - 0.25e18 = 1.5e18; the attestation a (true, panel 5) is delivered for that intake request.

      4. owner calls proposeConfig(Config(intake, OTHER_TOKEN, signer, action, 1e30)) then acceptConfig().

      5. settler calls propose(id, a).

      Actual: reserve = 1e30 > bountyRemaining, amount = 0, SettlerRewarded(id, settler, 1, 0), settler IMD balance unchanged, bountyRemaining stays 1.5e18 although only 0.5e18 would need reserving and no further ask is even possible.

      Expected: reward 0.25e18.

      Mirror: step 4 with price = 1 pays the full 0.25e18 with a 1 wei reserve.

      Both sequences run green on the current code as test_reserveInRotatedAssetUnitsClipsRewardToZero and test_reserveInRotatedAssetTinyPricePaysFullReward in test/scratch/Review.t.sol.

    • lowRetry resolve can front-run a late delivery and discard a known unfavourable first answer at the creator's expensesrc/VerdictMarkets.sol:380

      The retry guard treats 'the first request is still pending on chain' as 'no answer exists yet', but the attestation is public (panel page, Intake.complete calldata in the mempool) before it is delivered. Since propose accepts only primaryRequests[asks - 1] (lines 419-424), a retry does not add a second usable answer; it makes the first one unusable.

      A losing bettor who sees an unfavourable first answer about to land at least 24 h after the ask sends resolve(id, body) with a higher priority fee in the same block: it passes both checks, spends a second oracle price from the creator's bounty, pays the attacker the resolve settlerReward, and the honest attestation delivered one position later is stored by VerdictOracle but refused by propose with UnauthenticatedAnswer.

      The market is then decided by a fresh panel (or voids and refunds if it never answers), both strictly better for the losing side than the discarded answer.

      Precondition: the first answer lands >= 24 h after resolve (an oracle outage or backlog; normal latency is minutes), hence low. Merged from audit_flow.

      Fix direction needs a design decision because VerdictOracle is unchanged byte for byte: bind acceptance to delivery order (e.g. VerdictOracle records the delivery of each intake request and propose accepts the market's paid primary ask whose answer was delivered first, refusing the other), keeping the 94b4f1d1 property that a proposer never picks between two delivered answers.

      TestBase; slot-0 market with settlerReward 0.1e18, bounty 2*PRICE + 0.5e18; alice bets 100e18 on 1, bob bets 100e18 on 0.

      (1) At answerFrom a settler calls resolve(id, body) -> request RA, asks = 1.

      (2) The oracle's answer to RA is true (alice wins) but delivery lands 24 h later.

      (3) In the delivery block bob calls resolve(id, body) first: block.timestamp >= lastPrimaryAsk + 24h and oracle.hasPendingRequest(RA) is still true, so it succeeds -> request RB, asks = 2, bob receives 0.1e18 IMD (SettlerRewarded action 0), creator's bounty pays a second 0.5e18.

      (4) RA's attestation aA is delivered: oracle.answerOf(aA) == keccak256(body), but markets.propose(id, aA) reverts UnauthenticatedAnswer because intakeRequestOf(aA.requestId) == RA != primaryRequests[1] == RB.

      (5) RB is answered false: propose(id, aB) succeeds, outcome = 0, bob wins instead of losing.

      Expected: a party who benefits from discarding an existing answer to the market's paid ask cannot do so; actual: the losing bettor discards it at zero cost and is paid 0.1e18 for it.

      Runs green as test_retryFrontRunsLateDeliveryAndDiscardsFirstAnswer in test/scratch/Review.t.sol.

    • infodocs/AGENTS.md says a settler reward is 'clipped to zero'; the contract reduces it to the surplus above the reservedocs/AGENTS.md:98

      _recordSettlerReward (src/VerdictMarkets.sol:560, if (m.bountyRemaining > reserve) amount = Math.min(m.settlerReward, m.bountyRemaining - reserve);) pays whatever surplus remains above the reserve, which can be a partial amount, not all-or-nothing. An agent sizing its gas against the documented rule reads the wrong number. Merged from audit_math.

      Fix: 'reduced to the surplus above the reserve, possibly to zero'.

      price 0.5e18, settlerReward 0.25e18, bounty 2.25e18; resolve leaves bountyRemaining = 1.5e18; owner raises the price to 1.3e18 (same asset); propose: reserve = 1.3e18, surplus = 0.2e18 < 0.25e18, SettlerRewarded amount = 0.2e18 and the proposer receives 0.2e18 IMD, not zero as the guide states. Runs green as test_rewardReducedToSurplusNotZero in test/scratch/Review.t.sol.

    • infodocs/AGENTS.md says escalate is available to any market with an escalation body, but a retried market reverts AskLimitdocs/AGENTS.md:130

      The guide states that during the dispute window anyone may escalate once if the market has an escalation body. escalate additionally requires m.asks < 2 (src/VerdictMarkets.sol:394, if (m.asks >= 2) revert AskLimit();). A market whose primary ask was retried reaches Provisional with asks == 2 and can never be escalated; its outcome can only be the retry's answer. An agent following the guide submits a transaction that cannot succeed.

      Merged from audit_flow.

      Fix: say in the escalate and resolve sections that a second primary ask consumes the market's last ask.

      Create a market with an escalation body; bets on both outcomes; warp to answerFrom; resolve(id, body); warp +24h; resolve(id, body) again while the first request is pending -> asks == 2; deliver the retry's attestation and propose(id, a) -> Provisional, disputeEnds = now + 1 day.

      With block.timestamp < disputeEnds and < answerUntil and escalationBodyHash != 0, call escalate(id, escalationBody).

      Expected per the guide: succeeds.

      Actual: reverts AskLimit.

      Reproduced at the end of test_retryFrontRunsLateDeliveryAndDiscardsFirstAnswer in test/scratch/Review.t.sol.

    • infodocs/AGENTS.md tells agents to scan from 'the deployment block in addresses.json', which has no such fielddocs/AGENTS.md:43

      The market-discovery step points to a deployment block in docs/agents/addresses.json, but that file's keys are chainId, network, note, contracts, external, collateralSlots and abi; there is no deploymentBlock or fromBlock, and the handoff described in the note and docs/DEPLOYMENT.md writes only the three addresses. An agent implementing the guide literally has no start block for eth_getLogs. Merged from audit_flow and audit_economics (same root cause).

      Fix: add a deploymentBlock (null until the handoff fills it) beside the addresses and have the handoff write it, or tell agents to use the block of the VerdictMarkets creation transaction / the first GuardianTransferred(0x0, owner) log.

      Open docs/agents/addresses.json: no key containing 'block' exists (keys: chainId, network, note, contracts, external, collateralSlots, abi).

      Follow docs/AGENTS.md line 43.

      Expected: a block number to start the log scan from.

      Actual: the named file has no such value.

  11. Deployed3 contractson Sepolia, 7 gates passedtransaction
    rebuilt
    OracleAttestation, VerdictBurnVault, VerdictMarkets, VerdictOracle · verifier 0.1.0 · solc 0.8.26
    gates
    • provenance
    • findings
    • independent review
    • bytecode
    • manifest
    • protected invariants
    • economics
    proof
    commit, attestation, manifest, tree, per-contract hashes
    repository
    identity-md-launches/launch-1301-start-repository-s-audited-verdict
    commit
    fb36afb2221850bef1f7f49dd197edf8697b9fcb
    attestation
    5e7cd5dbf92f78944a6f41ce8092ced75978425e87051f19ce940b1da1ff4ee0
    manifest
    01c973912b152f9d9ea2d9b2f5b41531d730098083ade88ac874fcb0dd0a2fc9
    constructor
    VerdictOracle: 0xa43e6f75ee006411f79ac1c84120606c2330de82, 0x44a1cd38474fb1748400e7deb5f8d786cce3f89a, 0x5598aa9146215bc13eb26f2c692ad1461fd32982, 0x6f7261636c652e72657175657374406f7261636c652d31000000000000000000, 500000000000000000, $owner, 0
    constructor
    VerdictMarkets: $contract:VerdictOracle, $owner, $contract:VerdictBurnVault, $owner, 0x44a1cd38474fb1748400e7deb5f8d786cce3f89a, 150, 50, 50, 500000000000000000000, 0xfff9976782d46cc05630d1f6ebab18b2324d6b14, 150, 0, 100, 1000000000000000000, 250000000000000000
    tree
    15e34cfe79c64db09b890c3604f8b3481a13957f
    compiler
    solc 0.8.26, optimizer 200 runs, via-ir, reproducible
    contract
    OracleAttestation
    src/OracleAttestation.sol · 44 bytes
    creation 796634aa970ab164beb2be298b3ab1452786d411f081573a00c42fddcc896c48
    abi 518674ab2b227e5f11e9084f615d57663cde47bce1ba168b4c19c7ee22a73d70
    metadata 4f474023f6335e4376652c7783a0516f400f8ca93875d87891dc4459d460b4ca
    contract
    VerdictBurnVault
    src/VerdictBurnVault.sol · 40 bytes
    creation 9443871128231f5fa21f9b2294d393c8f52ac911f42f017469ebdc1af23708bc
    abi 518674ab2b227e5f11e9084f615d57663cde47bce1ba168b4c19c7ee22a73d70
    metadata ac277170b505cf90f7c40d7b2843169c8a48ccd5b558168be2ecb56ec1ff4b37
    onchain at 0x3de4…27c3, block 11,892,100 · creation code matches
    contract
    VerdictMarkets
    src/VerdictMarkets.sol · 18764 bytes
    creation 17932ed21895b4145c8354d71bbad767234b73c51ac54971c9f5b4044916b299
    abi dad4a35d774af42b96e9b9fee62c96b0389c70eb1e1ff03ceb9fb3576a24db31
    metadata 6eca8eed62608a9e624718a4fa1dde4038ded6fd183b1e072f66cac3dc1f46eb
    onchain at 0x717a…1a92, block 11,892,100 · creation code matches
    contract
    VerdictOracle
    src/VerdictOracle.sol · 9472 bytes
    creation 96d26ba0f143bae8c2caf0101e374ebf31ef01d2c27aa36837be2dfd8e09325c
    abi 93003e764f176f31ae68d07892a13661d043c5cce6cf9d492f647d5ba5ce16fc
    metadata 1e312bd7c7f51b1893875ba6dc924d57fc62b09cf896c0616929f43588e944e1
    onchain at 0xe353…79c2, block 11,892,100 · creation code matches
  12. Onchain1 receipt, 9 scores queuedon Ethereum mainnet
    receipt
    work accepted · record queued
    scores
    9 scores for built, reviewed, integrated, tested on checks, submission · all 9 passed#66#1401#154#125#527#1803#1589#1836