diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..248c35a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "foundry-lib/forge-std"] + path = foundry-lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/README.md b/README.md index d3e1f2b..8b9f267 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,34 @@ node deploy.mjs --chain base --fork # full deploy + smoke + end-to-end on a CONFIRM_MAINNET=base node deploy.mjs --chain base --broadcast # the real deploy ``` +## v2 (self-audit fixes, live on 9 chains) + +`contracts/v2/` is the redesigned set that fixes the 20.10.2026 self-audit findings (H-01..H-03, M-01..M-05, L-01..L-07). v2.1 is deployed on Arc, Base, Arbitrum, Avalanche, BNB Chain, Celo, Optimism, Sonic and Unichain (see [Deployments](#deployments)). The v1 set stays deployed as legacy on its 16 chains and is untouched. Design, finding-by-finding mapping and owner actions: `SELF-AUDIT-2026-10-20/v2-design.md` (Downloads). + +**v2.1 (23.10.2026, set `2026-10-23-v2.1`)** adds the fixes for the Olympix BugPoCer scan of `c47ed42` and the owner's option (a) +for the validator bond: `PredgeValidatorBondV2.recordScore` refuses any verdict that contradicts the on-chain evidence and reverts +`VerdictContradictsEvidence`. A pass (100) needs the provider to have submitted the output bytes (hashed on-chain by the job) with a +hash equal to the `expected` the client committed in the job's specHash; a fail (0) needs the opposite. A false verdict therefore +cannot be recorded at any bond size, including `bondBps = 0`. `challenge`, `wouldSlash`, `reclaim` and the dispute window are gone +(nothing is left to challenge); the bond is credited back to the staker with the verdict and only backs liveness: an operator that +stays silent on a submitted job forfeits it to the provider (`release`). `verdictFor(jobId, expected)` returns the only score +`recordScore` will accept. Constructor: `(owner, operator, job, minBond, bondBps)`; `setParams(minBond, bondBps)`. +The same deployer creates v2.1 at nonces 6..11 (`nonceBase` in `config/set.v2.json`), again the same addresses on Arc and Base. + +``` +FOUNDRY_CONFIG=foundry-v2.toml forge test # unit + fuzz + invariant + PoC tests (v1 attacks run against v1 and v2) +FOUNDRY_CONFIG=foundry-v2.toml forge coverage --no-match-coverage "(test/|foundry-lib|contracts/[A-Za-z]+\.sol)" +node --test test/ # plan, v1 bytecode match (network), v2 plan +node deploy.mjs --chain base --set v2 --fork # anvil fork of Base: deploy + honest path + every v1 attack sequence (must revert) +node scripts/fork-all.mjs --set v2 --no-e2e # gas / cost on every chain +node scripts/mutate-v2.mjs # mutation smoke run +node scripts/v2-params.mjs 5 # regenerate per-chain bond floors (about $5 each) from live prices +node scripts/v2-post-deploy.mjs --chains arc,base # after a v2.1 broadcast: archive v2.0 records (*.v2-0.json), verify leftovers, README +``` + +Real v2 runs (`--set v2` without `--fork`) refuse to start until the owner fills `config/v2.config.json` (new deployer address, the Safe, the ops key) +and, to broadcast, pass `--key-file `. v2 never reads `~/.predge-deployer`. `--fork` uses throwaway accounts only. + ## The set Order is the deployer's nonce. Contract *i* is created at nonce *i*. @@ -59,7 +87,7 @@ The tool enforces this: **Never use the deployer key for anything else.** Not even a plain transfer out until every chain has the whole set. -Canonical addresses for deployer `0x5d016C1B4d7A42eB7c982da1Ae7fFdB958CFCAe9`: +Canonical v1 addresses for deployer `0x5d016C1B4d7A42eB7c982da1Ae7fFdB958CFCAe9`: | Contract | Address (every chain) | |---|---| @@ -72,7 +100,42 @@ Canonical addresses for deployer `0x5d016C1B4d7A42eB7c982da1Ae7fFdB958CFCAe9`: ## Deployments -Live on **16 mainnets** since 18 Oct 2026, all six contracts at the canonical addresses above. Checked the same day with `eth_getCode` on every chain, and every contract is an exact match on [Sourcify](https://sourcify.dev). Full receipts (block, gas, constructor args, source hash) are in `deployments/.json`. +### v2 (current, v2.1): Arc (5042), Base (8453), Arbitrum (42161), Avalanche (43114), BNB Chain (56), Celo (42220), Optimism (10), Sonic (146), Unichain (130) + +Deployed 2026-10-23 from set `2026-10-23-v2.1` (deployer nonces 6..11). The same six addresses on both chains. All six are verified on Sourcify. Receipts: [`deployments/v2/arc.json`](deployments/v2/arc.json), [`deployments/v2/base.json`](deployments/v2/base.json). + +| Contract | Address (all nine chains) | Sourcify | Explorer | +|---|---|---|---| +| PredgeAgentValidatorV2 | `0xE4CFdE5dbD9afD518b9c59136939607505bF643D` | [Sourcify](https://sourcify.dev/#/lookup/0xE4CFdE5dbD9afD518b9c59136939607505bF643D) | [Arc](https://explorer.arc.io/address/0xE4CFdE5dbD9afD518b9c59136939607505bF643D), [Base](https://basescan.org/address/0xE4CFdE5dbD9afD518b9c59136939607505bF643D) | +| AgentJobV2 | `0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F` | [Sourcify](https://sourcify.dev/#/lookup/0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F) | [Arc](https://explorer.arc.io/address/0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F), [Base](https://basescan.org/address/0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F) | +| PredgeValidatorBondV2 | `0x97E960Fe3D7759af8f51A95319859E60Df554329` | [Sourcify](https://sourcify.dev/#/lookup/0x97E960Fe3D7759af8f51A95319859E60Df554329) | [Arc](https://explorer.arc.io/address/0x97E960Fe3D7759af8f51A95319859E60Df554329), [Base](https://basescan.org/address/0x97E960Fe3D7759af8f51A95319859E60Df554329) | +| PredgeSettlementV2 | `0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24` | [Sourcify](https://sourcify.dev/#/lookup/0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24) | [Arc](https://explorer.arc.io/address/0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24), [Base](https://basescan.org/address/0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24) | +| MockRefundProtocolV2 (demo refund protocol) | `0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9` | [Sourcify](https://sourcify.dev/#/lookup/0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9) | [Arc](https://explorer.arc.io/address/0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9), [Base](https://basescan.org/address/0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9) | +| PredgeRefundArbiterV2 | `0xf404F908cb1ba47C01AB8526B725aAA940557eA9` | [Sourcify](https://sourcify.dev/#/lookup/0xf404F908cb1ba47C01AB8526B725aAA940557eA9) | [Arc](https://explorer.arc.io/address/0xf404F908cb1ba47C01AB8526B725aAA940557eA9), [Base](https://basescan.org/address/0xf404F908cb1ba47C01AB8526B725aAA940557eA9) | + +- **26 Oct 2026, seven more chains:** Arbitrum, Avalanche, BNB Chain, Celo, Optimism, Sonic and Unichain, the same six addresses as above. The v2 deployer was fresh on these chains, so `scripts/v2-burn-nonces.mjs` first sent six 0-value self-transactions (nonces 0..5) and the set then landed at nonces 6..11, exactly as on Arc and Base. 30 read-only smoke checks passed on every chain and all 42 contracts are exact matches on Sourcify. Receipts: [`deployments/v2/optimism.json`](deployments/v2/optimism.json), [`deployments/v2/unichain.json`](deployments/v2/unichain.json), [`deployments/v2/arbitrum.json`](deployments/v2/arbitrum.json), [`deployments/v2/bnb.json`](deployments/v2/bnb.json), [`deployments/v2/avalanche.json`](deployments/v2/avalanche.json), [`deployments/v2/sonic.json`](deployments/v2/sonic.json), [`deployments/v2/celo.json`](deployments/v2/celo.json). +- **Owner:** `0x9084f5000E07C7133D6dA5eE4f271AB6D1821144` (TEMPORARY owner = Amir EOA 0x9084…1144 by owner decision; transferOwnership→acceptOwnership to the Safe once created.). **Operator** (hot ops key, rotatable by the owner): `0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292`. +- **Bond floor:** minimal (bps = 0), per-chain floor from `config/v2.chains.json`. Since v2.1 the validator bond backs liveness only: `recordScore` refuses any verdict that contradicts the on-chain evidence (pass needs the output bytes hashed on-chain and equal to the client-committed `expected`; fail needs the opposite), so a false verdict cannot be recorded at any bond size and there is no challenge window. +- **Status:** v2.1 contains the fixes for the Olympix BugPoCer scan of `c47ed42` (23 Oct 2026) and the option (a) verdict check. The signed verdict API (`api.predge.io`) is unchanged and remains the product. + +### v2.0 (DEPRECATED, 20-21 Oct 2026): do not use + +The first v2 set (`2026-10-20-v2`, commit `c47ed42`) is still on chain at the addresses below, because the contracts are not upgradeable. It is affected by the confirmed Olympix findings and held no funds when it was superseded. Nothing in Predge points at it. Records: [`deployments/v2/arc.v2-0.json`](deployments/v2/arc.v2-0.json), [`deployments/v2/base.v2-0.json`](deployments/v2/base.v2-0.json). + +| Contract | Deprecated address (Arc and Base) | +|---|---| +| PredgeAgentValidatorV2 | ~~`0xACC29BF9Ae35e51d36792C3B20D0Ac65f86Dd495`~~ | +| AgentJobV2 | ~~`0x550b360840C6614aD70E91CC6673FF3AB2dFd98C`~~ | +| PredgeValidatorBondV2 | ~~`0xa4BB68E55398c4EDD958DD6B32460D2018F78881`~~ | +| PredgeSettlementV2 | ~~`0x60991fE6a7afDcC79bDc43b754acE5c16158edb3`~~ | +| MockRefundProtocolV2 | ~~`0x2768dBC17728403BAE6D8838e4AeC63bE21a9dA7`~~ | +| PredgeRefundArbiterV2 | ~~`0x72FAEb8eE70816Dbf66722657cc4Bea314EB5FE3`~~ | + +### v1 (legacy): 16 mainnets + +**Legacy, known issues, holds no funds.** Kept deployed as a reference only. The v2 set above fixes the self-review findings in it. + +The v1 set went live on **16 mainnets** on 18 Oct 2026, all six contracts at the canonical addresses above. Checked the same day with `eth_getCode` on every chain, and every contract is an exact match on [Sourcify](https://sourcify.dev). Full receipts (block, gas, constructor args, source hash) are in `deployments/.json`. Creation transactions, in nonce order: diff --git a/config/set.v2.json b/config/set.v2.json new file mode 100644 index 0000000..cabe371 --- /dev/null +++ b/config/set.v2.json @@ -0,0 +1,20 @@ +{ + "_comment": "The Predge v2 set (contracts/v2). Same rules as set.json: contract i is created at deployer nonce nonceBase + i, so a fresh v2 deployer + this order gives the same addresses on every chain. Never reorder; append only. $owner is the Safe, $operator the hot ops key, $deployer holds NO role (it only pays gas). Chain-specific values (minBond, arbiterMinBond) come from config/v2.chains.json and do not change the addresses. Owner decision 20.10.2026: bonds MINIMAL, bondBps = arbiterBondBps = 0, so every bond is exactly the per-chain floor (~$5); the bps mechanism stays in the contracts and the Safe can raise it later with setParams (a paid bonded-verdict tier). 23.10.2026 (v2.1, Olympix fixes + option a): the v2 deployer already used nonces 0..5 on Arc and Base for the 20.10 set, so v2.1 is created at nonces 6..11 (nonceBase 6), again the same addresses on both chains; the validator bond has no dispute window any more (recordScore refuses a false verdict, so nothing is left to challenge).", + "version": "2026-10-23-v2.1", + "nonceBase": 6, + "params": { + "challengeWindow": "86400", + "evaluationWindow": "259200", + "bondBps": "0", + "arbiterBondBps": "0", + "identityRegistry": "0x0000000000000000000000000000000000000000" + }, + "plan": [ + { "name": "PredgeAgentValidatorV2", "file": "v2/PredgeAgentValidatorV2", "args": ["$owner", "$operator", "$identityRegistry"], "role": "ERC-8004 validation registry, request-before-response, domain-separated request hashes" }, + { "name": "AgentJobV2", "file": "v2/AgentJobV2", "args": ["$evaluationWindow"], "role": "ERC-8183-style job escrow with expiry and pull payments; no owner" }, + { "name": "PredgeValidatorBondV2", "file": "v2/PredgeValidatorBondV2", "args": ["$owner", "$operator", "$AgentJobV2", "$minBond", "$bondBps"], "role": "commit-reveal validator bond; the evaluator of AgentJobV2; records only verdicts that match the on-chain evidence" }, + { "name": "PredgeSettlementV2", "file": "v2/PredgeSettlementV2", "args": ["$owner"], "role": "pay-per-route receipts in the native token" }, + { "name": "MockRefundProtocolV2", "file": "v2/mocks/MockRefundProtocolV2", "args": ["$PredgeRefundArbiterV2"], "role": "DEMO ONLY stand-in Refund Protocol (moves no money); out of audit scope" }, + { "name": "PredgeRefundArbiterV2", "file": "v2/PredgeRefundArbiterV2", "args": ["$owner", "$operator", "$MockRefundProtocolV2", "$challengeWindow", "$arbiterMinBond", "$arbiterBondBps"], "role": "optimistic bonded refund arbiter; Safe is the court of last resort" } + ] +} diff --git a/config/v2.chains.json b/config/v2.chains.json new file mode 100644 index 0000000..165546f --- /dev/null +++ b/config/v2.chains.json @@ -0,0 +1,127 @@ +{ + "_comment": "Generated by scripts/v2-params.mjs. minBond / arbiterMinBond in native wei (18 decimals), ~usdTarget dollars each, 1 significant digit.", + "generatedAt": "2026-10-20T13:28:11.380Z", + "usdTarget": 5, + "chains": { + "ethereum": { + "native": "ETH", + "priceUsd": 2392.23, + "minBond": "2000000000000000", + "arbiterMinBond": "2000000000000000" + }, + "base": { + "native": "ETH", + "priceUsd": 2392.23, + "minBond": "2000000000000000", + "arbiterMinBond": "2000000000000000" + }, + "bnb": { + "native": "BNB", + "priceUsd": 730.57, + "minBond": "6000000000000000", + "arbiterMinBond": "6000000000000000" + }, + "celo": { + "native": "CELO", + "priceUsd": 0.081817, + "minBond": "60000000000000000000", + "arbiterMinBond": "60000000000000000000" + }, + "polygon": { + "native": "POL", + "priceUsd": 0.092243, + "minBond": "50000000000000000000", + "arbiterMinBond": "50000000000000000000" + }, + "optimism": { + "native": "ETH", + "priceUsd": 2392.23, + "minBond": "2000000000000000", + "arbiterMinBond": "2000000000000000" + }, + "arbitrum": { + "native": "ETH", + "priceUsd": 2392.23, + "minBond": "2000000000000000", + "arbiterMinBond": "2000000000000000" + }, + "avalanche": { + "native": "AVAX", + "priceUsd": 9.46, + "minBond": "500000000000000000", + "arbiterMinBond": "500000000000000000" + }, + "gnosis": { + "native": "xDAI", + "priceUsd": 1, + "minBond": "5000000000000000000", + "arbiterMinBond": "5000000000000000000" + }, + "linea": { + "native": "ETH", + "priceUsd": 2392.23, + "minBond": "2000000000000000", + "arbiterMinBond": "2000000000000000" + }, + "scroll": { + "native": "ETH", + "priceUsd": 2392.23, + "minBond": "2000000000000000", + "arbiterMinBond": "2000000000000000" + }, + "unichain": { + "native": "ETH", + "priceUsd": 2392.23, + "minBond": "2000000000000000", + "arbiterMinBond": "2000000000000000" + }, + "sonic": { + "native": "S", + "priceUsd": 0.056326, + "minBond": "80000000000000000000", + "arbiterMinBond": "80000000000000000000" + }, + "mantle": { + "native": "MNT", + "priceUsd": 0.527794, + "minBond": "9000000000000000000", + "arbiterMinBond": "9000000000000000000" + }, + "arc": { + "native": "USDC", + "priceUsd": 0.999869, + "minBond": "5000000000000000000", + "arbiterMinBond": "5000000000000000000" + }, + "monad": { + "native": "MON", + "priceUsd": 0.02064362, + "minBond": "200000000000000000000", + "arbiterMinBond": "200000000000000000000" + }, + "robinhood": { + "native": "ETH", + "priceUsd": 2392.23, + "minBond": "2000000000000000", + "arbiterMinBond": "2000000000000000" + }, + "rootstock": { + "native": "RBTC", + "priceUsd": 81371, + "minBond": "60000000000000", + "arbiterMinBond": "60000000000000" + }, + "plasma": { + "native": "XPL", + "priceUsd": 0.075746, + "minBond": "60000000000000000000", + "arbiterMinBond": "60000000000000000000" + }, + "stable": { + "native": "USDT0", + "priceUsd": 0.99895, + "minBond": "5000000000000000000", + "arbiterMinBond": "5000000000000000000" + } + } +} diff --git a/config/v2.config.json b/config/v2.config.json new file mode 100644 index 0000000..5605d9c --- /dev/null +++ b/config/v2.config.json @@ -0,0 +1,7 @@ +{ + "_comment": "Owner-supplied v2 identities. 20.10.2026: deployer and operator generated locally (keys in ~/.predge-deployer-v2/, 0600, never committed), nonce 0 on Arc and Base at creation; owner stays null until the Safe exists. `deployer` = a NEW key created by the owner (pays gas only, holds no role, nonce 0 on every chain). `owner` = the Safe multisig (on EVERY chain you deploy to, at the same address if you want the same wiring everywhere). `operator` = the hot ops key (rotatable by the owner). dry-run and --broadcast refuse to run while any of these is null. --fork ignores this file and uses throwaway anvil accounts.", + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "owner": "0x9084f5000e07c7133d6da5ee4f271ab6d1821144", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "_owner_note": "20.10.2026: TEMPORARY owner = Amir EOA 0x9084\u20261144 by owner decision; transferOwnership\u2192acceptOwnership to the Safe once created." +} diff --git a/contracts/v2/AgentJobV2.sol b/contracts/v2/AgentJobV2.sol new file mode 100644 index 0000000..9f65f18 --- /dev/null +++ b/contracts/v2/AgentJobV2.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IAgentJobV2} from "./interfaces/IAgentJobV2.sol"; +import {PullCredit} from "./access/PullCredit.sol"; + +/// @title AgentJobV2 +/// @notice A minimal ERC-8183-style agent job with escrowed native value and an evaluator named at +/// creation. The evaluator may be neither the client nor the provider (a trivial check: it +/// cannot detect two addresses controlled by one party). In Predge's deployment the evaluator is the +/// `PredgeValidatorBondV2` contract, which only accepts a verdict that agrees with the on-chain +/// deliverable hash and moves the money in the same transaction. +/// +/// Lifecycle: +/// createJob (client escrows value, names provider, evaluator, spec and an expiry) +/// submit (provider, strictly before `expiredAt`) +/// complete / reject (evaluator, strictly before `submittedAt + evaluationWindow`) +/// claimRefund (anyone may trigger; the CLIENT is credited) when +/// - the job is still Open at or after `expiredAt`, or +/// - the job is Submitted and the evaluator stayed silent until +/// `submittedAt + evaluationWindow`. +/// +/// Timeout policy (documented choice): if the evaluator never decides, the CLIENT is +/// refunded, as ERC-8183 does on expiry. The alternative (pay the provider by default) +/// would let a silent or bribed evaluator pay out unverified work. The cost of this +/// choice is borne by the provider, who did the work; providers should only work for +/// evaluators they trust to answer inside the window. +/// +/// Money never leaves by push. Every payout, whether to provider or client, is credited +/// to `credit[account]` and pulled with `withdraw` / `withdrawTo`, so a reverting or +/// blocklisted recipient can neither force an outcome nor lock another party's funds. +/// +/// @dev Native value only; amounts are in the chain's native wei. There is no owner, no +/// admin, no upgrade path and no mutable parameter: `evaluationWindow` is immutable. +/// Deliverable hashing: `sha256`. When the provider supplies the output bytes, the hash is +/// computed on-chain with the sha256 precompile (so the stored hash provably belongs to +/// those bytes); otherwise the provider's claimed hash is stored as given. +contract AgentJobV2 is IAgentJobV2, PullCredit { + /// @notice Seconds the evaluator has to decide after a submission. Immutable, so it cannot + /// be changed under a job that is already running. + uint64 public immutable evaluationWindow; + + uint256 public jobCount; + /// @notice Sum of every job's escrow that has not been credited yet (Open or Submitted). + uint256 public totalEscrowed; + + mapping(uint256 => Job) private _jobs; + + event JobCreated( + uint256 indexed jobId, + address indexed client, + address indexed evaluator, + address provider, + uint256 escrow, + uint64 expiredAt, + bytes32 specHash + ); + event Submitted(uint256 indexed jobId, address indexed provider, bytes32 deliverable, bool hashedOnChain); + event Completed(uint256 indexed jobId, address indexed evaluator, address indexed recipient, uint256 amount, bytes32 reason); + event Rejected(uint256 indexed jobId, address indexed evaluator, address indexed recipient, uint256 amount, bytes32 reason); + event Refunded(uint256 indexed jobId, address indexed caller, address indexed recipient, uint256 amount); + + error ZeroAddress(); + error ZeroEscrow(); + error BadExpiry(); + error BadWindow(); + error NotProvider(); + error NotEvaluator(); + error BadState(); + error PastDeadline(); + error NotExpired(); + error HashMismatch(); + error ZeroDeliverable(); + error EvaluatorNotIndependent(); + + /// @param evaluationWindow_ seconds the evaluator has after a submission; must be non-zero. + constructor(uint64 evaluationWindow_) { + if (evaluationWindow_ == 0) revert BadWindow(); + evaluationWindow = evaluationWindow_; + } + + /// @notice Client opens a job, escrows `msg.value`, and names the provider, the independent + /// evaluator, the acceptance-test commitment and the expiry. + /// @param provider the worker; paid on `complete` + /// @param evaluator the only address that can complete or reject + /// @param specHash the acceptance test commitment. For a job evaluated by `PredgeValidatorBondV2` it + /// must be `bond.specHashFor(keccak256(abi.encode(expected, salt)))`: the client + /// fixes the expected result here, hidden behind the salt. + /// @param expiredAt unix time; the provider must submit strictly before it, after it the + /// client can reclaim the escrow of a job that was never submitted + function createJob(address provider, address evaluator, bytes32 specHash, uint64 expiredAt) + external + payable + returns (uint256 jobId) + { + if (provider == address(0) || evaluator == address(0)) revert ZeroAddress(); + // Olympix 5.3.4: a client (or provider) that names itself evaluator would judge its own job. + if (evaluator == msg.sender || evaluator == provider) revert EvaluatorNotIndependent(); + if (msg.value == 0) revert ZeroEscrow(); + if (expiredAt <= block.timestamp) revert BadExpiry(); + jobId = ++jobCount; + totalEscrowed += msg.value; + Job storage j = _jobs[jobId]; + j.client = msg.sender; + j.provider = provider; + j.evaluator = evaluator; + j.escrow = msg.value; + j.expiredAt = expiredAt; + j.state = State.Open; + j.specHash = specHash; + emit JobCreated(jobId, msg.sender, evaluator, provider, msg.value, expiredAt, specHash); + } + + /// @notice Provider submits the deliverable, strictly before `expiredAt`. + /// @param deliverable sha256 of the output. Must be non-zero. + /// @param output optional output bytes. If non-empty, `sha256(output)` must equal + /// `deliverable` (reverts `HashMismatch` otherwise) and the job records that + /// the hash was verified on-chain. If empty, the claimed hash is stored. + function submit(uint256 jobId, bytes32 deliverable, bytes calldata output) external { + Job storage j = _jobs[jobId]; + if (j.state != State.Open) revert BadState(); + if (msg.sender != j.provider) revert NotProvider(); + if (block.timestamp >= j.expiredAt) revert PastDeadline(); + if (deliverable == bytes32(0)) revert ZeroDeliverable(); + bool onChain = output.length != 0; + if (onChain && sha256(output) != deliverable) revert HashMismatch(); + j.deliverable = deliverable; + j.hashedOnChain = onChain; + j.submittedAt = uint64(block.timestamp); + j.state = State.Submitted; + emit Submitted(jobId, msg.sender, deliverable, onChain); + } + + /// @notice ERC-8183 `complete`. Evaluator only, while the job is Submitted and the + /// evaluation window is still running. Credits the provider. + /// @param reason the evaluator's attestation commitment (Predge: keccak256 of the signed verdict) + function complete(uint256 jobId, bytes32 reason, bytes calldata) external { + Job storage j = _decide(jobId, reason); + j.state = State.Completed; + uint256 amt = _release(j, j.provider); + emit Completed(jobId, msg.sender, j.provider, amt, reason); + } + + /// @notice ERC-8183 `reject`. Evaluator only, same timing as `complete`. Credits the client. + function reject(uint256 jobId, bytes32 reason, bytes calldata) external { + Job storage j = _decide(jobId, reason); + j.state = State.Rejected; + uint256 amt = _release(j, j.client); + emit Rejected(jobId, msg.sender, j.client, amt, reason); + } + + /// @notice Refund the client of a job that ran out of time. Callable by anyone (the credit + /// always goes to the client): see the contract notes for the two cases. + function claimRefund(uint256 jobId) external { + Job storage j = _jobs[jobId]; + State s = j.state; + if (s == State.Open) { + if (block.timestamp < j.expiredAt) revert NotExpired(); + } else if (s == State.Submitted) { + if (block.timestamp < uint256(j.submittedAt) + evaluationWindow) revert NotExpired(); + } else { + revert BadState(); + } + j.state = State.Expired; + uint256 amt = _release(j, j.client); + emit Refunded(jobId, msg.sender, j.client, amt); + } + + /// @inheritdoc IAgentJobV2 + function getJob(uint256 jobId) external view returns (Job memory) { + return _jobs[jobId]; + } + + function _decide(uint256 jobId, bytes32 reason) private returns (Job storage j) { + j = _jobs[jobId]; + if (msg.sender != j.evaluator) revert NotEvaluator(); + if (j.state != State.Submitted) revert BadState(); + if (block.timestamp >= uint256(j.submittedAt) + evaluationWindow) revert PastDeadline(); + j.reason = reason; + } + + function _release(Job storage j, address to) private returns (uint256 amt) { + amt = j.escrow; + j.escrow = 0; + totalEscrowed -= amt; + _addCredit(to, amt); + } +} diff --git a/contracts/v2/PredgeAgentValidatorV2.sol b/contracts/v2/PredgeAgentValidatorV2.sol new file mode 100644 index 0000000..beb590f --- /dev/null +++ b/contracts/v2/PredgeAgentValidatorV2.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Operated} from "./access/PredgeAccess.sol"; + +/// @notice The one ERC-721 call the registry needs from an ERC-8004 Identity Registry. +interface IIdentityOwner { + function ownerOf(uint256 agentId) external view returns (address); +} + +/// @title PredgeAgentValidatorV2 +/// @notice An ERC-8004 Validation Registry shaped log of validation requests and verdicts, with +/// one property enforced by the chain: a request is timestamped BEFORE its response, and a +/// response is written once and never changes. +/// +/// What this contract does NOT do, so nobody reads more into it than is there: +/// * It does not verify the ed25519 attestation. The operator verifies it off-chain and +/// commits `responseHash`, the keccak256 of the signed bytes. +/// * It is not linked to `PredgeValidatorBondV2`. A registry response is a statement; the +/// bonded verdict, checked against the on-chain evidence, is the bond's `Scored` event and the +/// job it settled. +/// Consumers that want the bonded one must read the bond. +/// +/// Authorisation (fixes the v1 permissionless `validationRequest`): a request may be made +/// by the operator, or, when an Identity Registry address was set at construction, by the +/// current owner of the agent NFT. With no identity registry only the operator may +/// request. +/// +/// Domain separation: the caller supplies an acceptance-test DIGEST and the registry +/// derives `requestHash = keccak256(abi.encode(block.chainid, address(this), agentId, digest))`. +/// The same digest therefore yields a different request on every chain and registry, so a +/// request or verdict cannot be replayed across deployments, and a request is keyed to its +/// agent, so one agent cannot pre-claim another agent's digest (Olympix 5.3.3). This is the +/// one deviation from the ERC-8004 reference signature: the fourth argument is the digest, +/// and the derived hash is returned and emitted in the canonical event position. Use +/// `requestHashFor` to compute it off-chain. The registry is not linked to the bond: a bonded +/// job's specHash is `PredgeValidatorBondV2.specHashFor`, not this hash. +/// +/// @dev Roles: `owner` (Safe) rotates the operator; `operator` (hot key) answers requests. +/// Rotating the operator never rewrites a past verdict. +contract PredgeAgentValidatorV2 is Operated { + struct Validation { + address validator; // the operator at request time; replaced by the operator that writes the response + uint256 agentId; + bytes32 responseHash; + uint64 requestedAt; + uint64 respondedAt; // 0 while pending + uint8 response; // 0-100 + bool hasResponse; + string tag; + } + + /// @notice Optional ERC-8004 Identity Registry (the agent NFT). Zero = only the operator requests. + address public immutable identityRegistry; + + uint64 public requestCount; + uint64 public responseCount; + + /// @dev `getSummary` is O(n^2) in the number of hashes (it dedupes); cap the input. + uint256 public constant MAX_SUMMARY = 100; + + mapping(bytes32 => Validation) private _v; + mapping(bytes32 => string) public requestURIOf; + mapping(bytes32 => string) public responseURIOf; + + event ValidationRequest(address indexed validatorAddress, uint256 indexed agentId, string requestURI, bytes32 indexed requestHash); + event ValidationResponse( + address indexed validatorAddress, + uint256 indexed agentId, + bytes32 indexed requestHash, + uint8 response, + string responseURI, + bytes32 responseHash, + string tag + ); + /// @notice Extra context the canonical events lack: who asked, and the digest behind the hash. + event RequestRecorded(bytes32 indexed requestHash, bytes32 digest, address indexed requester); + /// @notice Extra context the canonical event lacks: who answered, and when. + event ResponseRecorded(bytes32 indexed requestHash, address indexed responder, uint64 respondedAt); + + error NotAuthorized(); + error WrongValidator(); + error ZeroHash(); + error AlreadyRequested(); + error NotRequested(); + error AlreadyResponded(); + error BadScore(); + error TooManyHashes(); + + /// @param owner_ the Safe + /// @param operator_ the hot key that answers requests + /// @param identityRegistry_ optional ERC-8004 Identity Registry; zero disables agent-owner requests + constructor(address owner_, address operator_, address identityRegistry_) Operated(owner_, operator_) { + identityRegistry = identityRegistry_; + } + + /// @notice The request hash this registry derives for an agent and an acceptance-test digest. + function requestHashFor(uint256 agentId, bytes32 digest) public view returns (bytes32) { + return keccak256(abi.encode(block.chainid, address(this), agentId, digest)); + } + + /// @notice Record a validation request before the outcome is knowable. + /// @param validatorAddress must equal the current `operator` (kept for ERC-8004 compatibility) + /// @param agentId the agent under validation (the NFT id when an identity registry is set) + /// @param requestURI pointer to the signed acceptance test + /// @param digest keccak256 of the signed acceptance-test bytes; must be non-zero + /// @return requestHash the domain-separated hash under which the request is stored + function validationRequest(address validatorAddress, uint256 agentId, string calldata requestURI, bytes32 digest) + external + returns (bytes32 requestHash) + { + if (digest == bytes32(0)) revert ZeroHash(); + if (msg.sender != operator && !_isAgentOwner(agentId)) revert NotAuthorized(); + if (validatorAddress != operator) revert WrongValidator(); + requestHash = requestHashFor(agentId, digest); + Validation storage v = _v[requestHash]; + if (v.requestedAt != 0) revert AlreadyRequested(); + + v.validator = validatorAddress; + v.agentId = agentId; + v.requestedAt = uint64(block.timestamp); + requestURIOf[requestHash] = requestURI; + requestCount += 1; + + emit ValidationRequest(validatorAddress, agentId, requestURI, requestHash); + emit RequestRecorded(requestHash, digest, msg.sender); + } + + /// @notice Record the verdict, once, for a request that exists. Operator only. The responding + /// operator is recorded as the validator (Olympix 5.3.10), so after a rotation the canonical + /// event and `getValidationStatus` name the key that actually answered. + /// @param response 0-100 (Predge: 100 delivered, 0 failed, 50 void) + function validationResponse( + bytes32 requestHash, + uint8 response, + string calldata responseURI, + bytes32 responseHash, + string calldata tag + ) external onlyOperator { + if (response > 100) revert BadScore(); + if (responseHash == bytes32(0)) revert ZeroHash(); + Validation storage v = _v[requestHash]; + if (v.requestedAt == 0) revert NotRequested(); + if (v.hasResponse) revert AlreadyResponded(); + + v.validator = msg.sender; + v.response = response; + v.responseHash = responseHash; + v.respondedAt = uint64(block.timestamp); + v.hasResponse = true; + v.tag = tag; + responseURIOf[requestHash] = responseURI; + responseCount += 1; + + emit ValidationResponse(v.validator, v.agentId, requestHash, response, responseURI, responseHash, tag); + emit ResponseRecorded(requestHash, msg.sender, uint64(block.timestamp)); + } + + /// @notice ERC-8004 `getValidationStatus`. + function getValidationStatus(bytes32 requestHash) + external + view + returns (address validatorAddress, uint256 agentId, uint8 response, bytes32 responseHash, string memory tag, uint256 lastUpdate) + { + Validation storage v = _v[requestHash]; + return (v.validator, v.agentId, v.response, v.responseHash, v.tag, v.respondedAt != 0 ? v.respondedAt : v.requestedAt); + } + + /// @notice True once a verdict is permanently recorded. + function isValidated(bytes32 requestHash) external view returns (bool) { + return _v[requestHash].hasResponse; + } + + /// @notice Seconds the request stayed committed before its verdict. 0 while pending, and also 0 if the + /// verdict landed in the same block as the request (chain timestamps have block granularity), so + /// a non-zero value proves the lead, but zero does not prove its absence. + function commitLeadTime(bytes32 requestHash) external view returns (uint64) { + Validation storage v = _v[requestHash]; + if (!v.hasResponse) return 0; + return v.respondedAt - v.requestedAt; + } + + /// @notice ERC-8004 `getSummary` for one agent over the supplied hashes. A hash that appears + /// more than once is counted once. + function getSummary(uint256 agentId, bytes32[] calldata requestHashes) + external + view + returns (uint64 count, uint8 averageResponse) + { + uint256 n = requestHashes.length; + if (n > MAX_SUMMARY) revert TooManyHashes(); + uint256 sum = 0; + for (uint256 i = 0; i < n; i++) { + bool seen = false; + for (uint256 k = 0; k < i; k++) { + if (requestHashes[k] == requestHashes[i]) { + seen = true; + break; + } + } + if (seen) continue; + Validation storage v = _v[requestHashes[i]]; + if (v.hasResponse && v.agentId == agentId) { + sum += v.response; + count += 1; + } + } + averageResponse = count == 0 ? 0 : uint8(sum / count); + } + + function _isAgentOwner(uint256 agentId) private view returns (bool) { + if (identityRegistry == address(0)) return false; + try IIdentityOwner(identityRegistry).ownerOf(agentId) returns (address o) { + return o == msg.sender; + } catch { + return false; + } + } +} diff --git a/contracts/v2/PredgeRefundArbiterV2.sol b/contracts/v2/PredgeRefundArbiterV2.sol new file mode 100644 index 0000000..d18028a --- /dev/null +++ b/contracts/v2/PredgeRefundArbiterV2.sol @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Operated} from "./access/PredgeAccess.sol"; +import {PullCredit} from "./access/PullCredit.sol"; + +/// @notice What the arbiter needs from a Refund Protocol. `paymentInfo` is the parties-and-amount +/// view the arbiter binds evidence and bond size to. Circle's production contract is not +/// known to expose exactly this function: against it, a thin read-only adapter that +/// implements `paymentInfo` and forwards the two write calls is required, and must be +/// reviewed before use. On our chains only `MockRefundProtocolV2` exists. +interface IRefundProtocolV2 { + /// @return payer the party refunded by `refundByArbiter` + /// @return payee the recipient of the payment + /// @return amount the payment amount, in the same native wei the bond is sized in + /// @return releaseTimestamp when the payee may withdraw; a refund is impossible after it + function paymentInfo(uint256 paymentID) + external + view + returns (address payer, address payee, uint256 amount, uint256 releaseTimestamp); + function refundByArbiter(uint256 paymentID) external; + function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external; +} + +/// @title PredgeRefundArbiterV2 +/// @notice A bonded, OPTIMISTIC arbiter for a Refund Protocol. The operator rules; nothing moves +/// until a challenge window has passed; the harmed party can freeze the ruling and escalate +/// it to the owner (a Safe); and a ruling the owner overturns costs the operator its bond. +/// +/// Flow, per payment: +/// 1. `fileEvidence`: the PAYER or the PAYEE (read from the Refund Protocol, not +/// chosen by the caller) files the hash of its own evidence. Write-once per party, and +/// only before a ruling exists. +/// 2. `rule` (operator): cites one party's filed evidence, picks Refund or Release, and +/// posts a bond of at least `max(minBond, ceil(amount * bondBps / 10000))`. The +/// operator may not itself be a party. Nothing executes yet. +/// 3. Within `window` seconds of the ruling, the party the ruling HARMS (the payee on a +/// Refund, the payer on a Release) may call `dispute` with counter-evidence. That +/// freezes the ruling and starts the owner's `RESOLUTION_TIMEOUT`. +/// 4a. Undisputed: after the window anyone calls `finalize`. The bond is credited back to +/// the staker and, for a Refund, `refundByArbiter` is called on the Refund Protocol. +/// 4b. Disputed: the owner calls `resolve(paymentID, finalDirection)`. If it equals the +/// ruling, the ruling is upheld exactly as in 4a. If not, the ruling is overturned: +/// the bond is credited to the harmed party (the disputer), and the owner's +/// direction is executed (a Refund is sent to the Refund Protocol). +/// 4c. If neither happens before `expiresAt` (owner silent, or the Refund Protocol keeps +/// reverting), anyone calls `expire`: the ruling is voided, NOTHING is executed, and +/// the bond returns to the staker. +/// +/// Trust, stated plainly: for disputed rulings the owner Safe is the court of last +/// resort. An operator that rules wrongly is only punished if the harmed party notices and +/// disputes inside the window, and the owner then agrees. The whole life of a ruling must fit +/// before the Refund Protocol's lockup ends (Olympix 5.2.1): `rule` requires, for EITHER +/// direction, `releaseTimestamp > now + window + RESOLUTION_TIMEOUT`, so a dispute filed at the +/// last second still leaves the owner its full resolution period, and a refund chosen by +/// `finalize` or `resolve` can still execute. `finalize` should be called promptly (the operator, +/// who wants its bond back, and the payer, who wants the refund, both have a reason to): once +/// `expiresAt` has passed anyone can `expire` the ruling instead, which executes nothing and +/// returns the bond. +/// +/// @dev Native value only, in the chain's native wei. The contract holds `totalBonded + +/// totalCredit` and never user payments. Parameters are bounded and not retroactive: the +/// window is snapshotted into each ruling, and the bond is fixed when posted. +contract PredgeRefundArbiterV2 is Operated, PullCredit { + enum Direction { + None, + Refund, + Release + } + + enum Status { + None, + Ruled, // window running, or elapsed and not yet finalized + Disputed, // frozen, waiting for the owner + Finalized, // upheld; bond returned; refund executed if Refund (final) + Overturned, // owner disagreed; bond paid to the harmed party (final) + Voided // expired without a decision; nothing executed; bond returned (final) + } + + struct Ruling { + address staker; // the operator key that ruled; the only address its bond is ever returned to + Status status; + Direction direction; + uint64 ruledAt; + uint64 window; // snapshot of `challengeWindow` + uint64 expiresAt; // after this, `expire` voids the ruling + address evidenceFrom; + address payer; + address payee; + uint256 bond; + bytes32 evidenceHash; + bytes32 counterEvidence; + } + + uint64 public constant MIN_WINDOW = 1 hours; + uint64 public constant MAX_WINDOW = 30 days; + uint64 public constant RESOLUTION_TIMEOUT = 14 days; + uint16 public constant MAX_BPS = 10_000; + /// @notice Upper bound for a recipient lockup forwarded by `setLockupSeconds`. + uint256 public constant MAX_LOCKUP = 365 days; + + IRefundProtocolV2 public immutable refundProtocol; + + uint64 public challengeWindow; + uint256 public minBond; + uint16 public bondBps; + + uint256 public totalBonded; + uint64 public rulingCount; + uint64 public overturnCount; + + mapping(uint256 => Ruling) private _rulings; + /// @notice paymentID => party => hash of the evidence that party filed. Write-once. + mapping(uint256 => mapping(address => bytes32)) public filedEvidence; + + event EvidenceFiled(uint256 indexed paymentID, address indexed party, bytes32 evidenceHash); + event Ruled( + uint256 indexed paymentID, + address indexed staker, + address indexed evidenceFrom, + bytes32 evidenceHash, + Direction direction, + uint256 bond, + uint64 windowEnd + ); + event Disputed(uint256 indexed paymentID, address indexed disputer, bytes32 counterEvidence, uint64 expiresAt); + event Finalized(uint256 indexed paymentID, address indexed staker, Direction direction, uint256 bond); + event Overturned(uint256 indexed paymentID, address indexed harmed, Direction finalDirection, uint256 bond); + event Voided(uint256 indexed paymentID, address indexed staker, uint256 bond); + event ParamsUpdated(uint64 challengeWindow, uint256 minBond, uint16 bondBps); + event LockupForwarded(address indexed recipient, uint256 lockupSeconds); + + error ZeroHash(); + error BadParams(); + error BadDirection(); + error UnknownPayment(); + error NotAParty(); + error OperatorIsParty(); + error EvidenceClosed(); + error EvidenceAlreadyFiled(uint256 paymentID, address party); + error NoEvidenceFiled(uint256 paymentID, address party); + error AlreadyRuled(uint256 paymentID); + error BondTooSmall(uint256 required); + error LockupTooShort(); + error BadStatus(); + error WindowOpen(); + error WindowClosed(); + error NotHarmedParty(); + error NotExpired(); + error ResolutionExpired(); + + /// @param owner_ the Safe: resolves disputes, sets bounded parameters, rotates the operator + /// @param operator_ the hot key that rules + /// @param refundProtocol_ the Refund Protocol (or adapter) this arbiter drives; immutable + /// @param challengeWindow_ seconds a ruling stays disputable, within [MIN_WINDOW, MAX_WINDOW] + /// @param minBond_ absolute bond floor in native wei, non-zero + /// @param bondBps_ bond as basis points of the payment amount, at most 10000 + constructor(address owner_, address operator_, address refundProtocol_, uint64 challengeWindow_, uint256 minBond_, uint16 bondBps_) + Operated(owner_, operator_) + { + if (refundProtocol_ == address(0)) revert ZeroAddress(); + refundProtocol = IRefundProtocolV2(refundProtocol_); + _setParams(challengeWindow_, minBond_, bondBps_); + } + + /// @notice Owner-only, bounded, not retroactive: existing rulings keep their window and bond. + function setParams(uint64 challengeWindow_, uint256 minBond_, uint16 bondBps_) external onlyOwner { + _setParams(challengeWindow_, minBond_, bondBps_); + } + + /// @notice Pass a recipient lockup change through to the Refund Protocol (owner only). Bounded + /// (Olympix 5.3.12): longer than `challengeWindow + RESOLUTION_TIMEOUT`, so a ruling on a + /// payment made under it can complete before release, and at most `MAX_LOCKUP`. + function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external onlyOwner { + if (recipientLockupSeconds <= uint256(challengeWindow) + RESOLUTION_TIMEOUT || recipientLockupSeconds > MAX_LOCKUP) { + revert BadParams(); + } + emit LockupForwarded(recipient, recipientLockupSeconds); + refundProtocol.setLockupSeconds(recipient, recipientLockupSeconds); + } + + /// @notice The bond a ruling on a payment of `amount` must post right now. + function requiredBond(uint256 amount) public view returns (uint256) { + uint256 scaled = (amount * bondBps + MAX_BPS - 1) / MAX_BPS; + return scaled > minBond ? scaled : minBond; + } + + /// @notice A party to the payment files the hash of its own evidence. Write-once, and only + /// before the ruling. The caller must be the payer or the payee per the Refund Protocol. + function fileEvidence(uint256 paymentID, bytes32 evidenceHash) external { + if (evidenceHash == bytes32(0)) revert ZeroHash(); + if (_rulings[paymentID].status != Status.None) revert EvidenceClosed(); + (address payer, address payee,,) = refundProtocol.paymentInfo(paymentID); + if (msg.sender != payer && msg.sender != payee) revert NotAParty(); + if (filedEvidence[paymentID][msg.sender] != bytes32(0)) revert EvidenceAlreadyFiled(paymentID, msg.sender); + filedEvidence[paymentID][msg.sender] = evidenceHash; + emit EvidenceFiled(paymentID, msg.sender, evidenceHash); + } + + /// @notice Rule on a payment: cite a party's filed evidence, choose a direction, post the bond. + /// Nothing executes until `finalize` or `resolve`. + /// @param evidenceFrom the payer or payee whose filed evidence the ruling rests on + function rule(uint256 paymentID, address evidenceFrom, bytes32 evidenceHash, Direction direction) + external + payable + onlyOperator + { + if (evidenceHash == bytes32(0)) revert ZeroHash(); + if (direction == Direction.None) revert BadDirection(); + Ruling storage r = _rulings[paymentID]; + if (r.status != Status.None) revert AlreadyRuled(paymentID); + + (address payer, address payee, uint256 amount, uint256 releaseTimestamp) = refundProtocol.paymentInfo(paymentID); + if (payee == address(0)) revert UnknownPayment(); + if (msg.sender == payer || msg.sender == payee) revert OperatorIsParty(); + if (evidenceFrom != payer && evidenceFrom != payee) revert NotAParty(); + if (filedEvidence[paymentID][evidenceFrom] != evidenceHash) revert NoEvidenceFiled(paymentID, evidenceFrom); + uint256 required = requiredBond(amount); + if (msg.value < required) revert BondTooSmall(required); + uint64 window = challengeWindow; + // The whole ruling (window, then a dispute's full resolution period) must end before the payment is + // released, for BOTH directions: a Refund executes at finalize/resolve, and a Release can be overturned + // into a Refund by the owner. Otherwise a free last-second dispute could run the clock out. + if (releaseTimestamp <= block.timestamp + window + RESOLUTION_TIMEOUT) revert LockupTooShort(); + + r.staker = msg.sender; + r.status = Status.Ruled; + r.direction = direction; + r.ruledAt = uint64(block.timestamp); + r.window = window; + r.expiresAt = uint64(block.timestamp) + window + RESOLUTION_TIMEOUT; + r.evidenceFrom = evidenceFrom; + r.payer = payer; + r.payee = payee; + r.bond = msg.value; + r.evidenceHash = evidenceHash; + totalBonded += msg.value; + rulingCount += 1; + emit Ruled(paymentID, msg.sender, evidenceFrom, evidenceHash, direction, msg.value, uint64(block.timestamp) + window); + } + + /// @notice The harmed party (payee on a Refund, payer on a Release) files counter-evidence + /// inside the window. This freezes the ruling and hands it to the owner. + function dispute(uint256 paymentID, bytes32 counterEvidence) external { + if (counterEvidence == bytes32(0)) revert ZeroHash(); + Ruling storage r = _rulings[paymentID]; + if (r.status != Status.Ruled) revert BadStatus(); + if (block.timestamp >= uint256(r.ruledAt) + r.window) revert WindowClosed(); + if (msg.sender != _harmed(r)) revert NotHarmedParty(); + r.status = Status.Disputed; + r.counterEvidence = counterEvidence; + r.expiresAt = uint64(block.timestamp) + RESOLUTION_TIMEOUT; + emit Disputed(paymentID, msg.sender, counterEvidence, r.expiresAt); + } + + /// @notice After an undisputed window, return the bond to the staker and execute a Refund. + /// Anyone may call. If the Refund Protocol reverts, this reverts too and the ruling + /// can only be voided via `expire`. + function finalize(uint256 paymentID) external { + Ruling storage r = _rulings[paymentID]; + if (r.status != Status.Ruled) revert BadStatus(); + if (block.timestamp < uint256(r.ruledAt) + r.window) revert WindowOpen(); + _uphold(paymentID, r); + } + + /// @notice Owner decides a disputed ruling. `finalDirection` equal to the ruling upholds it; + /// any other direction overturns it and slashes the bond to the harmed party. The + /// final direction is executed either way (a Refund calls the Refund Protocol). + function resolve(uint256 paymentID, Direction finalDirection) external onlyOwner { + if (finalDirection == Direction.None) revert BadDirection(); + Ruling storage r = _rulings[paymentID]; + if (r.status != Status.Disputed) revert BadStatus(); + if (block.timestamp >= r.expiresAt) revert ResolutionExpired(); + if (finalDirection == r.direction) { + _uphold(paymentID, r); + return; + } + address harmed = _harmed(r); + uint256 bond = r.bond; + r.status = Status.Overturned; + r.bond = 0; + totalBonded -= bond; + overturnCount += 1; + _addCredit(harmed, bond); + emit Overturned(paymentID, harmed, finalDirection, bond); + if (finalDirection == Direction.Refund) refundProtocol.refundByArbiter(paymentID); + } + + /// @notice Void a ruling nobody decided in time. Nothing is executed; the bond returns to the + /// staker. Anyone may call once `expiresAt` has passed. + function expire(uint256 paymentID) external { + Ruling storage r = _rulings[paymentID]; + if (r.status != Status.Ruled && r.status != Status.Disputed) revert BadStatus(); + if (block.timestamp < r.expiresAt) revert NotExpired(); + uint256 bond = r.bond; + r.status = Status.Voided; + r.bond = 0; + totalBonded -= bond; + _addCredit(r.staker, bond); + emit Voided(paymentID, r.staker, bond); + } + + /// @notice The full ruling record for a payment (all-zero if none). + function rulings(uint256 paymentID) external view returns (Ruling memory) { + return _rulings[paymentID]; + } + + /// @notice The party the ruling in `paymentID` would harm, or zero if there is no ruling. + function harmedParty(uint256 paymentID) external view returns (address) { + Ruling storage r = _rulings[paymentID]; + return r.status == Status.None ? address(0) : _harmed(r); + } + + function _uphold(uint256 paymentID, Ruling storage r) private { + uint256 bond = r.bond; + r.status = Status.Finalized; + r.bond = 0; + totalBonded -= bond; + _addCredit(r.staker, bond); + emit Finalized(paymentID, r.staker, r.direction, bond); + if (r.direction == Direction.Refund) refundProtocol.refundByArbiter(paymentID); + } + + function _harmed(Ruling storage r) private view returns (address) { + return r.direction == Direction.Refund ? r.payee : r.payer; + } + + function _setParams(uint64 challengeWindow_, uint256 minBond_, uint16 bondBps_) private { + if (challengeWindow_ < MIN_WINDOW || challengeWindow_ > MAX_WINDOW || minBond_ == 0 || bondBps_ > MAX_BPS) + revert BadParams(); + challengeWindow = challengeWindow_; + minBond = minBond_; + bondBps = bondBps_; + emit ParamsUpdated(challengeWindow_, minBond_, bondBps_); + } +} diff --git a/contracts/v2/PredgeSettlementV2.sol b/contracts/v2/PredgeSettlementV2.sol new file mode 100644 index 0000000..3382372 --- /dev/null +++ b/contracts/v2/PredgeSettlementV2.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Owned2Step} from "./access/PredgeAccess.sol"; + +/// @title PredgeSettlementV2 +/// @notice Receipt layer for Predge's pay-per-call data API: an agent pays for a route in the +/// chain's native token and this contract emits a `Paid` receipt. +/// @dev Amounts are in native wei of the chain (on Arc the native token is USDC with 18 +/// decimals at the protocol level; the ERC-20 interface of Arc's USDC uses 6, so never +/// mix the two units). The owner (a Safe) withdraws accumulated payments to a non-zero +/// address; there is no other admin power and no upgrade path. Forced value (selfdestruct +/// or coinbase) is included in `withdraw`, so it cannot be stranded. +contract PredgeSettlementV2 is Owned2Step { + /// @param payer who paid + /// @param route keccak256 of the route id + /// @param amount native wei paid + /// @param timestamp block time + /// @param meta optional request reference + event Paid(address indexed payer, bytes32 indexed route, uint256 amount, uint256 timestamp, string meta); + event Withdrawn(address indexed to, uint256 amount); + + error ZeroPayment(); + error WithdrawFailed(); + + /// @param owner_ the Safe that may withdraw + constructor(address owner_) Owned2Step(owner_) {} + + /// @notice Pay for a route; emits a `Paid` receipt. + function payForRoute(bytes32 route, string calldata meta) external payable { + if (msg.value == 0) revert ZeroPayment(); + emit Paid(msg.sender, route, msg.value, block.timestamp, meta); + } + + /// @notice Owner withdraws the whole balance to `to`. Reverts for the zero address. + function withdraw(address payable to) external onlyOwner { + if (to == address(0)) revert ZeroAddress(); + uint256 bal = address(this).balance; + emit Withdrawn(to, bal); + (bool ok,) = to.call{value: bal}(""); + if (!ok) revert WithdrawFailed(); + } + + /// @notice A bare transfer still produces a receipt, under the route `bytes32("direct")`. + receive() external payable { + if (msg.value == 0) revert ZeroPayment(); + emit Paid(msg.sender, bytes32("direct"), msg.value, block.timestamp, ""); + } +} diff --git a/contracts/v2/PredgeValidatorBondV2.sol b/contracts/v2/PredgeValidatorBondV2.sol new file mode 100644 index 0000000..dd799aa --- /dev/null +++ b/contracts/v2/PredgeValidatorBondV2.sol @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Operated} from "./access/PredgeAccess.sol"; +import {PullCredit} from "./access/PullCredit.sol"; +import {IAgentJobV2} from "./interfaces/IAgentJobV2.sol"; + +/// @title PredgeValidatorBondV2 +/// @notice Capital behind a verdict, and the EVALUATOR of an `AgentJobV2`. The job names this +/// contract as its evaluator, so the verdict recorded here is the thing that moves the +/// escrow. +/// +/// The acceptance test is deterministic: the job's deliverable counts as DELIVERED only if the +/// provider submitted the output bytes (so the job computed `sha256(output)` on-chain, +/// `hashedOnChain == true`) and that hash equals, by RAW EQUALITY, the `expected` value the +/// CLIENT fixed when it created the job. A hash the provider merely claimed, with no bytes, never +/// counts as delivered. Nothing is hashed again here. +/// +/// A FALSE VERDICT CANNOT BE RECORDED (owner decision 23.10.2026, option a). Both sides of the +/// comparison are on-chain at `recordScore`: the hash the chain computed and the `expected` the +/// client committed to. So `recordScore` checks the score against them and reverts +/// `VerdictContradictsEvidence` unless score 100 <=> delivered and score 0 <=> not delivered. This +/// holds for any bond size, including `bondBps = 0`, and needs no challenger, no watcher and no +/// dispute window: there is nothing to challenge, so there is no `challenge` and the bond is +/// credited back to the staker in the same transaction as the verdict. +/// +/// Who fixes `expected` (Olympix 5.1.1): the client. A job evaluated by this contract must have +/// `specHash == specHashFor(keccak256(abi.encode(expected, salt)))`, which the client chooses at +/// `createJob`. The operator learns `(expected, salt)` from the client off-chain, checks it against +/// the job before staking, and can only commit to that value. +/// +/// Flow: +/// 1. `stakeAndCommit(requestHash, commitment, jobId)` (operator). Allowed ONLY while +/// the job is Open, i.e. before any deliverable exists. `commitment` is +/// `keccak256(abi.encode(expected, salt))` and must be the one the client bound into the +/// job's specHash (`requestHash == specHashFor(commitment)`). The bond must be at least +/// `max(minBond, bondBps * escrow / 10000)`. +/// 2. The provider submits to the job (with the output bytes, or the job can only fail). +/// 3. `recordScore(requestHash, score, expected, salt, reason)` (operator). Requires the +/// job to be Submitted, the reveal to match the commitment, and the score to agree with +/// the on-chain evidence. It then calls `complete` (score 100) or `reject` (score 0) on +/// the job, so the verdict and the money move together, and credits the bond back to the +/// staker. +/// 4. `release(requestHash)` (anyone) if the job expired with no verdict. If the provider never +/// submitted, nothing was owed and the bond goes back to the staker. If the provider DID submit +/// and the operator stayed silent until the evaluation window closed (Olympix 5.2.2), the +/// bond is FORFEITED to the provider: silence on submitted work is not free. +/// +/// What the bond is for, after option (a): LIVENESS, not correctness. Correctness is enforced by +/// `recordScore`. The bond is what the operator loses if it stays silent on a submitted job. +/// +/// Honest limits, stated so they are not mistaken for guarantees: +/// * The client defines `expected`. A provider should obtain `(expected, salt)` from the client +/// and check it against the job's specHash before working; knowing `expected` does not +/// let anyone pass without the real bytes, because only an on-chain hash counts. +/// * A pass needs the output bytes in calldata; large outputs cost the provider gas. +/// * On silence the provider receives the bond, not the escrow: the job's timeout policy +/// still refunds the escrow to the client. With `bondBps = 0` the bond is a flat floor. +/// * The operator's capital is locked until the job ends. A client picks the job's +/// `expiredAt`, so an operator should read it before staking. +/// * Only the deterministic `sha256(deliverable) == expected` test is covered. Subjective +/// or "void" outcomes are not bonded. +/// +/// @dev Native value only, in the chain's native wei. Roles: `owner` (Safe) sets bounded +/// parameters and rotates `operator`; `operator` (hot key) stakes and scores. Parameters +/// are not retroactive: the bond amount is fixed when staked. Payouts are pull-based. The +/// contract holds, at all times, at least `totalBonded + totalCredit`. +contract PredgeValidatorBondV2 is Operated, PullCredit { + enum Status { + None, + Committed, // bond posted, no verdict yet + Scored, // verdict recorded (it matches the evidence), bond credited back to the staker (final) + Released, // job expired before any submission, bond credited back to the staker (final) + Forfeited // job expired after a submission with no verdict, bond credited to the provider (final) + } + + struct Stake { + address staker; // the operator key that posted the bond; the only party it is ever paid back to + Status status; + uint8 score; // 100 delivered / 0 not delivered, set when scored + uint64 stakedAt; + uint64 scoredAt; // set when scored; always >= the job's submittedAt + uint256 jobId; + uint256 bond; // the posted bond while Committed; zero once closed + bytes32 commitment; // keccak256(abi.encode(expected, salt)) + bytes32 expected; // zero until revealed at recordScore + } + + uint16 public constant MAX_BPS = 10_000; + + IAgentJobV2 public immutable job; + + /// @notice Absolute floor for a bond, in native wei. Applies to stakes made after a change. + uint256 public minBond; + /// @notice Bond as basis points of the job escrow (10000 = 100%). Applies to future stakes. + uint16 public bondBps; + + /// @notice Sum of bonds currently held (Committed). + uint256 public totalBonded; + + mapping(bytes32 => Stake) public stakes; + + event Committed(bytes32 indexed requestHash, uint256 indexed jobId, address indexed staker, bytes32 commitment, uint256 bond); + event Scored(bytes32 indexed requestHash, uint256 indexed jobId, uint8 score, bytes32 expected, address staker, uint256 bond); + event Released(bytes32 indexed requestHash, address indexed staker, uint256 bond); + event Forfeited(bytes32 indexed requestHash, address indexed provider, uint256 bond); + event ParamsUpdated(uint256 minBond, uint16 bondBps); + + error ZeroHash(); + error BadParams(); + error AlreadyCommitted(); + error NotCommitted(); + error BadStatus(); + error NotOpen(); + error NotSubmitted(); + error JobNotBound(); + error ProviderIsOperator(); + error ClientIsOperator(); + error CommitmentNotBound(); + error BondTooSmall(uint256 required); + error BadScore(); + error BadReveal(); + error VerdictContradictsEvidence(); + error JobNotExpired(); + + /// @param owner_ the Safe: sets bounded parameters, rotates the operator + /// @param operator_ the hot key that stakes and scores + /// @param job_ the `AgentJobV2` this bond evaluates; immutable + /// @param minBond_ absolute floor for a bond, non-zero + /// @param bondBps_ bond as basis points of escrow, at most 10000 + constructor(address owner_, address operator_, address job_, uint256 minBond_, uint16 bondBps_) + Operated(owner_, operator_) + { + if (job_ == address(0)) revert ZeroAddress(); + job = IAgentJobV2(job_); + _setParams(minBond_, bondBps_); + } + + /// @notice Owner-only parameter change, bounded and NOT retroactive: existing stakes keep + /// the bond they were made with. + function setParams(uint256 minBond_, uint16 bondBps_) external onlyOwner { + _setParams(minBond_, bondBps_); + } + + /// @notice The bond a stake against `escrow` must post right now: `max(minBond, ceil(escrow * bondBps / 10000))`. + function requiredBond(uint256 escrow) public view returns (uint256) { + uint256 scaled = (escrow * bondBps + MAX_BPS - 1) / MAX_BPS; + return scaled > minBond ? scaled : minBond; + } + + /// @notice The specHash a client must give a job evaluated by this contract, for a commitment + /// `keccak256(abi.encode(expected, salt))`. Domain separated by chain and by this contract. + function specHashFor(bytes32 commitment) public view returns (bytes32) { + return keccak256(abi.encode(block.chainid, address(this), commitment)); + } + + /// @notice The only verdict `recordScore` accepts for job `jobId` against `expected`: 100 if the + /// provider submitted the bytes and their on-chain sha256 equals `expected`, otherwise 0. + /// Meaningful once the job is Submitted. + function verdictFor(uint256 jobId, bytes32 expected) public view returns (uint8) { + IAgentJobV2.Job memory j = job.getJob(jobId); + return _delivered(j, expected) ? 100 : 0; + } + + /// @notice Post the bond and commit the hidden acceptance test, while the job is still Open. + /// @param requestHash the job's `specHash`; must equal `specHashFor(commitment)` + /// @param commitment `keccak256(abi.encode(expected, salt))` as fixed by the client in the specHash + /// @param jobId the job this verdict will settle; this contract must be its evaluator + function stakeAndCommit(bytes32 requestHash, bytes32 commitment, uint256 jobId) external payable onlyOperator { + if (requestHash == bytes32(0) || commitment == bytes32(0)) revert ZeroHash(); + Stake storage s = stakes[requestHash]; + if (s.status != Status.None) revert AlreadyCommitted(); + // The client fixed `expected` (hidden behind the salt) in the specHash: the operator cannot pick its own. + if (requestHash != specHashFor(commitment)) revert CommitmentNotBound(); + + IAgentJobV2.Job memory j = job.getJob(jobId); + // Only while Open and unexpired: the stake must exist before the work does. + if (j.state != IAgentJobV2.State.Open || block.timestamp >= j.expiredAt) revert NotOpen(); + if (j.specHash != requestHash || j.evaluator != address(this)) revert JobNotBound(); + // A provider that is also the operator would decide whether to deliver and when to stay silent. + if (j.provider == msg.sender) revert ProviderIsOperator(); + // A client that is also the operator would judge its own job (Olympix 5.2.3). + if (j.client == msg.sender) revert ClientIsOperator(); + uint256 required = requiredBond(j.escrow); + if (msg.value < required) revert BondTooSmall(required); + + s.staker = msg.sender; + s.status = Status.Committed; + s.stakedAt = uint64(block.timestamp); + s.jobId = jobId; + s.bond = msg.value; + s.commitment = commitment; + totalBonded += msg.value; + emit Committed(requestHash, jobId, msg.sender, commitment, msg.value); + } + + /// @notice Reveal the committed test, record the verdict, settle the job with it, and credit the + /// bond back to the staker. Reverts `VerdictContradictsEvidence` on a false verdict. + /// @param score 100 = delivered (job `complete`s, provider is credited), 0 = not delivered + /// (job `reject`s, client is credited). Must equal `verdictFor(jobId, expected)`. + /// @param expected,salt the preimage of the commitment + /// @param reason the attestation commitment passed to the job (Predge: keccak256 of the signed verdict) + function recordScore(bytes32 requestHash, uint8 score, bytes32 expected, bytes32 salt, bytes32 reason) + external + onlyOperator + { + if (score != 0 && score != 100) revert BadScore(); + Stake storage s = stakes[requestHash]; + if (s.status == Status.None) revert NotCommitted(); + if (s.status != Status.Committed) revert BadStatus(); + if (keccak256(abi.encode(expected, salt)) != s.commitment) revert BadReveal(); + // Re-checked here, not only at stake time: after an operator rotation the NEW operator must not be able + // to score a job it is a party to, while inheriting the old operator's stake. + IAgentJobV2.Job memory j = job.getJob(s.jobId); + if (j.provider == msg.sender) revert ProviderIsOperator(); + if (j.client == msg.sender) revert ClientIsOperator(); + // Scoring needs a submission: the verdict is judged against a deliverable that exists. + if (j.state != IAgentJobV2.State.Submitted) revert NotSubmitted(); + // Option (a): the evidence decides. A pass needs an on-chain hash equal to `expected`; a fail needs the opposite. + if ((score == 100) != _delivered(j, expected)) revert VerdictContradictsEvidence(); + + s.expected = expected; + s.score = score; + s.scoredAt = uint64(block.timestamp); + uint256 bond = _close(s, Status.Scored, s.staker); + emit Scored(requestHash, s.jobId, score, expected, s.staker, bond); + + if (score == 100) job.complete(s.jobId, reason, ""); + else job.reject(s.jobId, reason, ""); + } + + /// @notice Close a stake whose job expired without a verdict. Anyone may trigger it. If the provider + /// never submitted, the bond is credited back to the staker. If the provider submitted and the + /// operator let the evaluation window close without scoring, the bond is credited to the + /// provider instead. + function release(bytes32 requestHash) external { + Stake storage s = stakes[requestHash]; + if (s.status == Status.None) revert NotCommitted(); + if (s.status != Status.Committed) revert BadStatus(); + IAgentJobV2.Job memory j = job.getJob(s.jobId); + if (j.state != IAgentJobV2.State.Expired) revert JobNotExpired(); + if (j.submittedAt == 0) { + uint256 bond = _close(s, Status.Released, s.staker); + emit Released(requestHash, s.staker, bond); + } else { + uint256 bond = _close(s, Status.Forfeited, j.provider); + emit Forfeited(requestHash, j.provider, bond); + } + } + + /// @dev Delivered means: the bytes were hashed on-chain AND the hash equals the client's `expected`. + function _delivered(IAgentJobV2.Job memory j, bytes32 expected) private pure returns (bool) { + return j.hashedOnChain && j.deliverable == expected; + } + + function _close(Stake storage s, Status to, address payee) private returns (uint256 bond) { + bond = s.bond; + s.status = to; + s.bond = 0; + totalBonded -= bond; + _addCredit(payee, bond); + } + + function _setParams(uint256 minBond_, uint16 bondBps_) private { + if (minBond_ == 0 || bondBps_ > MAX_BPS) revert BadParams(); + minBond = minBond_; + bondBps = bondBps_; + emit ParamsUpdated(minBond_, bondBps_); + } +} diff --git a/contracts/v2/access/PredgeAccess.sol b/contracts/v2/access/PredgeAccess.sol new file mode 100644 index 0000000..2bad8ec --- /dev/null +++ b/contracts/v2/access/PredgeAccess.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/// @title Owned2Step +/// @notice Minimal two-step ownership, the same hand-over model as OpenZeppelin v5 `Ownable2Step`. +/// The owner is passed to the constructor (the intended owner is a Safe multisig), so the +/// deployer key never holds a role. There is deliberately NO `renounceOwnership`: a +/// renounced owner could never rotate a compromised operator or resolve a dispute. +/// @dev Two steps: `transferOwnership(next)` nominates, `acceptOwnership()` (called by `next`) +/// completes. Nominating `address(0)` cancels a pending nomination. A wrong address can +/// therefore never take ownership, because it has to prove it can sign. +abstract contract Owned2Step { + /// @notice Current owner. Config and dispute resolution only; never moves user funds. + address public owner; + /// @notice Nominated owner, or zero. + address public pendingOwner; + + event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + error NotOwner(); + error NotPendingOwner(); + error ZeroAddress(); + + modifier onlyOwner() { + if (msg.sender != owner) revert NotOwner(); + _; + } + + /// @param owner_ initial owner; must not be zero. + constructor(address owner_) { + if (owner_ == address(0)) revert ZeroAddress(); + owner = owner_; + emit OwnershipTransferred(address(0), owner_); + } + + /// @notice Nominate `newOwner`. Passing `address(0)` cancels a pending nomination. + function transferOwnership(address newOwner) external onlyOwner { + pendingOwner = newOwner; + emit OwnershipTransferStarted(owner, newOwner); + } + + /// @notice Complete the hand-over. Only the nominated address can call this. + function acceptOwnership() external { + if (msg.sender != pendingOwner) revert NotPendingOwner(); + emit OwnershipTransferred(owner, msg.sender); + owner = msg.sender; + pendingOwner = address(0); + } +} + +/// @title Operated +/// @notice Adds the hot "operator" role (the day-to-day validator / arbiter key) under an +/// `Owned2Step` owner. The owner can rotate the operator; the operator can never touch +/// ownership or configuration. +/// @dev Rotation is not retroactive by design: the contracts that pay out snapshot the staker +/// address on each stake or ruling, so rotating this key never lets a new key take an old +/// key's bond, and never strands the bond either. It can strand an in-flight VERDICT +/// (Olympix 5.3.8): a Committed stake in `PredgeValidatorBondV2` can only be scored by the new +/// key if it is given the old commitment's `(expected, salt)`. If it is not, the job times out, +/// the client is refunded and the bond is forfeited to the provider. Rotate with no stake in +/// flight, or hand the preimages over, unless the old key is compromised. +abstract contract Operated is Owned2Step { + /// @notice The hot operations key. Rotatable by the owner. + address public operator; + + event OperatorUpdated(address indexed previousOperator, address indexed newOperator); + + error NotOperator(); + + modifier onlyOperator() { + if (msg.sender != operator) revert NotOperator(); + _; + } + + /// @param owner_ initial owner (Safe); must not be zero. + /// @param operator_ initial operator; must not be zero. + constructor(address owner_, address operator_) Owned2Step(owner_) { + if (operator_ == address(0)) revert ZeroAddress(); + operator = operator_; + emit OperatorUpdated(address(0), operator_); + } + + /// @notice Rotate the operator key. Owner only; the new key must be non-zero. + function setOperator(address newOperator) external onlyOwner { + if (newOperator == address(0)) revert ZeroAddress(); + emit OperatorUpdated(operator, newOperator); + operator = newOperator; + } +} diff --git a/contracts/v2/access/PullCredit.sol b/contracts/v2/access/PullCredit.sol new file mode 100644 index 0000000..ab3353c --- /dev/null +++ b/contracts/v2/access/PullCredit.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/// @title PullCredit +/// @notice Pull-payment ledger shared by the Predge v2 contracts. Whoever is owed value is +/// credited here and withdraws it themselves, so a recipient that reverts or is +/// blocklisted can never force an outcome or lock anybody else's funds. +/// @dev `totalCredit` is the sum of all outstanding credits; the inheriting contract's native +/// balance is always at least `totalCredit` plus whatever it still escrows or bonds. +/// Checks-effects-interactions: the credit is zeroed and the event emitted before the call. +abstract contract PullCredit { + /// @notice Withdrawable balance per account. + mapping(address => uint256) public credit; + /// @notice Sum of all credits not yet withdrawn. + uint256 public totalCredit; + + event Withdrawn(address indexed account, address indexed to, uint256 amount); + + error NothingToWithdraw(); + error TransferFailed(); + error ZeroRecipient(); + + /// @notice Withdraw the caller's whole credit to the caller. + function withdraw() external { + _withdraw(payable(msg.sender)); + } + + /// @notice Withdraw the caller's whole credit to `to`, e.g. when the caller's own address + /// cannot receive value (blocklisted or a contract without a receive function). + function withdrawTo(address payable to) external { + if (to == address(0)) revert ZeroRecipient(); + _withdraw(to); + } + + function _addCredit(address to, uint256 amount) internal { + credit[to] += amount; + totalCredit += amount; + } + + function _withdraw(address payable to) private { + uint256 amt = credit[msg.sender]; + if (amt == 0) revert NothingToWithdraw(); + credit[msg.sender] = 0; + totalCredit -= amt; + emit Withdrawn(msg.sender, to, amt); + (bool ok,) = to.call{value: amt}(""); + if (!ok) revert TransferFailed(); + } +} diff --git a/contracts/v2/adapters/CircleRefundProtocolAdapter.sol b/contracts/v2/adapters/CircleRefundProtocolAdapter.sol new file mode 100644 index 0000000..c34ae14 --- /dev/null +++ b/contracts/v2/adapters/CircleRefundProtocolAdapter.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IRefundProtocolV2} from "../PredgeRefundArbiterV2.sol"; +import {ICircleRefundProtocol} from "../interfaces/ICircleRefundProtocol.sol"; + +interface IPredgeRefundArbiterView { + function refundProtocol() external view returns (address); +} + +/// @title CircleRefundProtocolAdapter +/// @notice Lets an unchanged `PredgeRefundArbiterV2` act as the arbiter of Circle's Refund Protocol. +/// The adapter is the address set as `arbiter` in Circle's constructor; it accepts write calls +/// only from the one Predge arbiter bound to it, and it translates the read side: +/// +/// Predge `paymentInfo` <- Circle `payments(id)` +/// payer <- refundTo (who a refund is paid to) +/// payee <- to +/// amount <- amount * amountScale (USDC 6 decimals -> native wei) +/// releaseTimestamp <- releaseTimestamp +/// +/// A payment that does not exist, is already refunded, or has been partly withdrawn reads as +/// all-zero, so no evidence can be filed and no ruling can be made on it. +/// +/// Wiring is a three-way cycle (Circle needs the adapter address, the Predge arbiter needs the +/// adapter address, the adapter needs both), so the adapter is deployed first and `bind` is +/// called once by its deployer after the other two exist. `bind` checks both directions of the +/// wiring and can never be called again. +/// +/// @dev Holds no funds and has no owner. It does not expose Circle's `earlyWithdrawByArbiter`, +/// `depositArbiterFunds` or `withdrawArbiterFunds`: a refund is paid only from the recipient's +/// balance inside Circle's contract. If that balance cannot cover it, Circle reverts, the +/// Predge `finalize`/`resolve` reverts with it, and the ruling can only be voided by `expire`. +contract CircleRefundProtocolAdapter is IRefundProtocolV2 { + address public immutable deployer; + /// @notice Multiplier from the token's units to the native wei the Predge bond is sized in + /// (1e12 on Arc: USDC ERC-20 has 6 decimals, native USDC gas has 18). + uint256 public immutable amountScale; + + ICircleRefundProtocol public circle; + address public predgeArbiter; + + event Bound(address indexed circle, address indexed predgeArbiter); + event RefundForwarded(uint256 indexed paymentID); + event RefundAlreadyDone(uint256 indexed paymentID); + event LockupForwarded(address indexed recipient, uint256 lockupSeconds); + + error ZeroAddress(); + error ZeroScale(); + error NotDeployer(); + error AlreadyBound(); + error NotBound(); + error NotCircleArbiter(); + error ArbiterNotWired(); + error NotPredgeArbiter(); + + constructor(uint256 amountScale_) { + if (amountScale_ == 0) revert ZeroScale(); + deployer = msg.sender; + amountScale = amountScale_; + } + + /// @notice One-time wiring, deployer only. Requires `circle_.arbiter() == this` and + /// `predgeArbiter_.refundProtocol() == this`. + function bind(address circle_, address predgeArbiter_) external { + if (msg.sender != deployer) revert NotDeployer(); + if (address(circle) != address(0)) revert AlreadyBound(); + if (circle_ == address(0) || predgeArbiter_ == address(0)) revert ZeroAddress(); + if (ICircleRefundProtocol(circle_).arbiter() != address(this)) revert NotCircleArbiter(); + if (IPredgeRefundArbiterView(predgeArbiter_).refundProtocol() != address(this)) revert ArbiterNotWired(); + circle = ICircleRefundProtocol(circle_); + predgeArbiter = predgeArbiter_; + emit Bound(circle_, predgeArbiter_); + } + + modifier onlyPredgeArbiter() { + if (msg.sender != predgeArbiter || predgeArbiter == address(0)) revert NotPredgeArbiter(); + _; + } + + /// @inheritdoc IRefundProtocolV2 + function paymentInfo(uint256 paymentID) + external + view + returns (address payer, address payee, uint256 amount, uint256 releaseTimestamp) + { + if (address(circle) == address(0)) revert NotBound(); + (address to, uint256 amt, uint256 release, address refundTo, uint256 withdrawn, bool refunded) = + circle.payments(paymentID); + if (to == address(0) || refunded || withdrawn != 0) return (address(0), address(0), 0, 0); + return (refundTo, to, amt * amountScale, release); + } + + /// @notice Forward a Predge refund to Circle. If the payment was already refunded (for example the + /// recipient called `refundByRecipient` during the window), the outcome the ruling asks for + /// already holds, so this returns instead of reverting. + function refundByArbiter(uint256 paymentID) external onlyPredgeArbiter { + (,,,,, bool refunded) = circle.payments(paymentID); + if (refunded) { + emit RefundAlreadyDone(paymentID); + return; + } + emit RefundForwarded(paymentID); + circle.refundByArbiter(paymentID); + } + + /// @notice Forward a lockup change. Predge's own bound is (window + 14 days, 365 days]; Circle's + /// cap of 180 days also applies and reverts above it. + function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external onlyPredgeArbiter { + emit LockupForwarded(recipient, recipientLockupSeconds); + circle.setLockupSeconds(recipient, recipientLockupSeconds); + } +} diff --git a/contracts/v2/interfaces/IAgentJobV2.sol b/contracts/v2/interfaces/IAgentJobV2.sol new file mode 100644 index 0000000..2ce25ae --- /dev/null +++ b/contracts/v2/interfaces/IAgentJobV2.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/// @notice The slice of `AgentJobV2` that other Predge contracts read and drive. +interface IAgentJobV2 { + /// @dev Forward-only, except that `Open` and `Submitted` may each end in `Expired`. + enum State { + None, + Open, + Submitted, + Completed, + Rejected, + Expired + } + + struct Job { + address client; // pays; refunded on reject / expiry + address provider; // does the work; paid on complete + address evaluator; // the only address that can complete or reject + uint256 escrow; // native value held for this job until it ends + uint64 expiredAt; // the provider must submit strictly before this + uint64 submittedAt; // 0 until submitted + State state; + bool hashedOnChain; // true if the deliverable hash was computed on-chain from submitted bytes + bytes32 specHash; // the committed acceptance test (bond.specHashFor(commitment) for bonded jobs) + bytes32 deliverable; // sha256 of the provider's output (zero until submitted) + bytes32 reason; // the evaluator's attestation commitment (zero until decided) + } + + function getJob(uint256 jobId) external view returns (Job memory); + function complete(uint256 jobId, bytes32 reason, bytes calldata optParams) external; + function reject(uint256 jobId, bytes32 reason, bytes calldata optParams) external; +} diff --git a/contracts/v2/interfaces/ICircleRefundProtocol.sol b/contracts/v2/interfaces/ICircleRefundProtocol.sol new file mode 100644 index 0000000..aef900f --- /dev/null +++ b/contracts/v2/interfaces/ICircleRefundProtocol.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/// @notice The subset of Circle's Refund Protocol (github.com/circlefin/refund-protocol, +/// src/RefundProtocol.sol, read 01.11.2026) that Predge touches. Written by Predge from the +/// public function signatures; no Circle code is copied or imported. Only signatures are +/// declared, so this file carries no Circle implementation. +/// +/// Facts about the real contract that shape the adapter (all from reading its source): +/// - `arbiter` is set once in the constructor; there is no setter. Every arbiter-only call +/// (`refundByArbiter`, `setLockupSeconds`, `earlyWithdrawByArbiter`, `depositArbiterFunds`, +/// `withdrawArbiterFunds`) checks `msg.sender == arbiter`. +/// - There is no `paymentInfo`; the public `payments(id)` getter returns the struct fields +/// `(to, amount, releaseTimestamp, refundTo, withdrawnAmount, refunded)`. The address that +/// called `pay` is NOT stored: the party a refund goes to is `refundTo`, which the current +/// `refundTo` may change at any time with `updateRefundTo`. +/// - `amount` is in the ERC-20's own units (USDC: 6 decimals). +/// - `refundByArbiter` does not check `releaseTimestamp`; it reverts `PaymentRefunded` if the +/// payment was already refunded, and `InsufficientFunds` if neither the recipient's balance +/// nor the arbiter's own deposited balance covers the amount. +/// - `setLockupSeconds` reverts above `MAX_LOCKUP_SECONDS` (180 days). +/// - Payment ids start at 0 (`nonce` starts at 0). +interface ICircleRefundProtocol { + function arbiter() external view returns (address); + function fiatToken() external view returns (address); + function nonce() external view returns (uint256); + function MAX_LOCKUP_SECONDS() external view returns (uint256); + function lockupSeconds(address recipient) external view returns (uint256); + function balances(address account) external view returns (uint256); + function debts(address recipient) external view returns (uint256); + + function payments(uint256 paymentID) + external + view + returns ( + address to, + uint256 amount, + uint256 releaseTimestamp, + address refundTo, + uint256 withdrawnAmount, + bool refunded + ); + + function pay(address to, uint256 amount, address refundTo) external; + function withdraw(uint256[] calldata paymentIDs) external; + function refundByRecipient(uint256 paymentID) external; + function updateRefundTo(uint256 paymentID, address newRefundTo) external; + + // arbiter-only + function refundByArbiter(uint256 paymentID) external; + function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external; + function depositArbiterFunds(uint256 amount) external; + function withdrawArbiterFunds(uint256 amount) external; +} diff --git a/contracts/v2/mocks/MockCircleRefundProtocol.sol b/contracts/v2/mocks/MockCircleRefundProtocol.sol new file mode 100644 index 0000000..7c5fd80 --- /dev/null +++ b/contracts/v2/mocks/MockCircleRefundProtocol.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/// @notice TEST / DEMO ONLY. Predge's own re-implementation of the behaviour of Circle's Refund +/// Protocol that the adapter depends on, written from the public interface and behaviour +/// described in `ICircleRefundProtocol`. It is NOT Circle's code and is not audited. +/// Same function names, argument order, getter layout and error names as Circle's contract, +/// so the adapter and tests exercise the real call shapes. Left out: EIP-712 +/// `earlyWithdrawByArbiter` (not used by Predge) and recipient debt bookkeeping beyond the +/// arbiter-funds fallback. +interface IERC20Min { + function transfer(address to, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} + +contract MockCircleRefundProtocol { + struct Payment { + address to; + uint256 amount; + uint256 releaseTimestamp; + address refundTo; + uint256 withdrawnAmount; + bool refunded; + } + + uint256 public constant MAX_LOCKUP_SECONDS = 180 days; + + address public immutable arbiter; + address public immutable fiatToken; + uint256 public nonce; + + mapping(address => uint256) public lockupSeconds; + mapping(uint256 => Payment) public payments; + mapping(address => uint256) public balances; + mapping(address => uint256) public debts; + + event PaymentCreated(uint256 indexed paymentID, address indexed to, uint256 amount, uint256 releaseTimestamp, address indexed refundTo); + event Refund(uint256 indexed paymentID, address indexed refundTo, uint256 amount); + event RefundToUpdated(uint256 indexed paymentID, address indexed oldRefundTo, address indexed newRefundTo); + event Withdrawal(address indexed to, uint256 amount); + + error CallerNotAllowed(); + error PaymentIsStillLocked(uint256 paymentID); + error RefundToIsZeroAddress(); + error InsufficientFunds(); + error PaymentRefunded(uint256 paymentID); + error LockupSecondsExceedsMax(); + + modifier onlyArbiter() { + if (msg.sender != arbiter) revert CallerNotAllowed(); + _; + } + + constructor(address arbiter_, address token_) { + arbiter = arbiter_; + fiatToken = token_; + } + + function pay(address to, uint256 amount, address refundTo) external { + if (refundTo == address(0)) revert RefundToIsZeroAddress(); + uint256 release = block.timestamp + lockupSeconds[to]; + require(IERC20Min(fiatToken).transferFrom(msg.sender, address(this), amount), "transferFrom"); + uint256 id = nonce++; + payments[id] = Payment(to, amount, release, refundTo, 0, false); + balances[to] += amount; + emit PaymentCreated(id, to, amount, release, refundTo); + } + + function refundByRecipient(uint256 paymentID) external { + Payment storage p = payments[paymentID]; + if (msg.sender != p.to) revert CallerNotAllowed(); + if (balances[p.to] < p.amount) revert InsufficientFunds(); + balances[p.to] -= p.amount; + _refund(paymentID, p); + } + + /// @dev Like Circle's: no release-time check; paid from the recipient's balance, else from the + /// arbiter's deposited balance (which then becomes the recipient's debt). + function refundByArbiter(uint256 paymentID) external onlyArbiter { + Payment storage p = payments[paymentID]; + if (balances[p.to] >= p.amount) { + balances[p.to] -= p.amount; + } else { + if (balances[arbiter] < p.amount) revert InsufficientFunds(); + balances[arbiter] -= p.amount; + debts[p.to] += p.amount; + } + _refund(paymentID, p); + } + + function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external onlyArbiter { + if (recipientLockupSeconds > MAX_LOCKUP_SECONDS) revert LockupSecondsExceedsMax(); + lockupSeconds[recipient] = recipientLockupSeconds; + } + + function depositArbiterFunds(uint256 amount) external onlyArbiter { + require(IERC20Min(fiatToken).transferFrom(msg.sender, address(this), amount), "transferFrom"); + balances[arbiter] += amount; + } + + function withdrawArbiterFunds(uint256 amount) external onlyArbiter { + if (balances[arbiter] < amount) revert InsufficientFunds(); + balances[arbiter] -= amount; + require(IERC20Min(fiatToken).transfer(arbiter, amount), "transfer"); + } + + function withdraw(uint256[] calldata paymentIDs) external { + uint256 total; + for (uint256 i; i < paymentIDs.length; ++i) { + Payment storage p = payments[paymentIDs[i]]; + if (p.to != msg.sender) revert CallerNotAllowed(); + if (block.timestamp < p.releaseTimestamp) revert PaymentIsStillLocked(paymentIDs[i]); + if (p.refunded) revert PaymentRefunded(paymentIDs[i]); + total += p.amount - p.withdrawnAmount; + p.withdrawnAmount = p.amount; + } + if (balances[msg.sender] < total) revert InsufficientFunds(); + balances[msg.sender] -= total; + require(IERC20Min(fiatToken).transfer(msg.sender, total), "transfer"); + emit Withdrawal(msg.sender, total); + } + + function updateRefundTo(uint256 paymentID, address newRefundTo) external { + if (newRefundTo == address(0)) revert RefundToIsZeroAddress(); + Payment storage p = payments[paymentID]; + if (msg.sender != p.refundTo) revert CallerNotAllowed(); + emit RefundToUpdated(paymentID, p.refundTo, newRefundTo); + p.refundTo = newRefundTo; + } + + function _refund(uint256 paymentID, Payment storage p) private { + if (p.refunded) revert PaymentRefunded(paymentID); + p.refunded = true; + require(IERC20Min(fiatToken).transfer(p.refundTo, p.amount), "transfer"); + emit Refund(paymentID, p.refundTo, p.amount); + } +} diff --git a/contracts/v2/mocks/MockRefundProtocolV2.sol b/contracts/v2/mocks/MockRefundProtocolV2.sol new file mode 100644 index 0000000..daed788 --- /dev/null +++ b/contracts/v2/mocks/MockRefundProtocolV2.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/// @notice DEMO / TEST ONLY stand-in for a Refund Protocol. It holds NO funds and moves NO +/// money: `refundByArbiter` only flips a flag and emits an event, so a "refund" here +/// proves the arbiter made the call and nothing more. It exposes the payment parties the +/// v2 arbiter binds evidence to. Anyone can create a payment record; that is intended for +/// demos and is the reason this contract must never be treated as a payment rail. +contract MockRefundProtocolV2 { + struct Payment { + address payer; + address payee; + uint256 amount; + uint256 releaseTimestamp; + bool refunded; + } + + address public immutable arbiter; + uint256 public paymentCount; + uint256 public refundCount; + mapping(uint256 => Payment) public payments; + mapping(address => uint256) public lockupSeconds; + + event PaymentRecorded(uint256 indexed paymentID, address indexed payer, address indexed payee, uint256 amount, uint256 releaseTimestamp); + event RefundByArbiter(uint256 indexed paymentID, address indexed caller); + event LockupSet(address indexed recipient, uint256 lockupSeconds); + + error CallerNotArbiter(); + error UnknownPayment(); + error AlreadyRefunded(); + error AlreadyReleased(); + error ZeroAddress(); + + constructor(address arbiter_) { + if (arbiter_ == address(0)) revert ZeroAddress(); + arbiter = arbiter_; + } + + /// @notice Record a payment's parties (no value moves). Demo only. + function recordPayment(address payer, address payee, uint256 amount, uint256 releaseTimestamp) + external + returns (uint256 paymentID) + { + if (payer == address(0) || payee == address(0)) revert ZeroAddress(); + paymentID = ++paymentCount; + payments[paymentID] = Payment(payer, payee, amount, releaseTimestamp, false); + emit PaymentRecorded(paymentID, payer, payee, amount, releaseTimestamp); + } + + function paymentInfo(uint256 paymentID) + external + view + returns (address payer, address payee, uint256 amount, uint256 releaseTimestamp) + { + Payment storage p = payments[paymentID]; + return (p.payer, p.payee, p.amount, p.releaseTimestamp); + } + + function refundByArbiter(uint256 paymentID) external { + if (msg.sender != arbiter) revert CallerNotArbiter(); + Payment storage p = payments[paymentID]; + if (p.payee == address(0)) revert UnknownPayment(); + if (p.refunded) revert AlreadyRefunded(); + if (block.timestamp >= p.releaseTimestamp) revert AlreadyReleased(); + p.refunded = true; + refundCount += 1; + emit RefundByArbiter(paymentID, msg.sender); + } + + function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external { + if (msg.sender != arbiter) revert CallerNotArbiter(); + lockupSeconds[recipient] = recipientLockupSeconds; + emit LockupSet(recipient, recipientLockupSeconds); + } +} diff --git a/contracts/v2/mocks/MockUSDC.sol b/contracts/v2/mocks/MockUSDC.sol new file mode 100644 index 0000000..28aeb8b --- /dev/null +++ b/contracts/v2/mocks/MockUSDC.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +/// @notice TEST ONLY 6-decimal ERC-20 with open minting. Never deploy outside a local test. +contract MockUSDC { + string public constant name = "Mock USDC"; + string public constant symbol = "mUSDC"; + uint8 public constant decimals = 6; + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + function mint(address to, uint256 amount) external { + totalSupply += amount; + balanceOf[to] += amount; + emit Transfer(address(0), to, amount); + } + + function approve(address spender, uint256 amount) external returns (bool) { + allowance[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + function transfer(address to, uint256 amount) external returns (bool) { + _move(msg.sender, to, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + uint256 a = allowance[from][msg.sender]; + if (a != type(uint256).max) allowance[from][msg.sender] = a - amount; + _move(from, to, amount); + return true; + } + + function _move(address from, address to, uint256 amount) private { + balanceOf[from] -= amount; + balanceOf[to] += amount; + emit Transfer(from, to, amount); + } +} diff --git a/deploy.mjs b/deploy.mjs index d6f2ff6..e6b5887 100755 --- a/deploy.mjs +++ b/deploy.mjs @@ -6,15 +6,22 @@ // CONFIRM_MAINNET=base node deploy.mjs --chain base --broadcast // the real thing: deploy, verify source, read-only smoke, deployments/base.json. // Options: --rpc (override), --no-verify, --no-e2e (fork), --keep-anvil (fork) +// +// --set v2 the v2 contract set (contracts/v2, config/set.v2.json). Roles (deployer / owner Safe / operator) come from +// config/v2.config.json and must be filled in by the owner; --fork uses throwaway anvil accounts and also runs +// the v1 attack sequences, which must revert. A real v2 --broadcast needs --key-file . +// v2 NEVER reads ~/.predge-deployer. Outputs go to deployments/v2/ and deployments/fork-v2/. import { spawn } from "node:child_process"; import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs"; import { createServer } from "node:net"; import { formatEther, Wallet } from "ethers"; -import { chainConfig, pickRpc, providerFor, withRetry, rawRpc, waitReceipt, SET } from "./lib/chains.mjs"; +import { chainConfig, pickRpc, providerFor, withRetry, rawRpc, waitReceipt, SET, SET_V2 } from "./lib/chains.mjs"; import { loadDeployer } from "./lib/key.mjs"; import { resolvePlan } from "./lib/plan.mjs"; import { maskImmutables, SOLC_VERSION, SETTINGS } from "./lib/compile.mjs"; import { readOnly, e2e } from "./lib/smoke.mjs"; +import { readOnlyV2, e2eV2 } from "./lib/smoke-v2.mjs"; +import { realRoles, forkRoles, chainParams, anvilWallets, v2KeyFile } from "./lib/v2.mjs"; import { verifyStep } from "./lib/verify.mjs"; import { l1FeeWei } from "./lib/fees.mjs"; @@ -22,6 +29,11 @@ const argv = process.argv.slice(2); const flag = (f) => argv.includes(f); const opt = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; }; const ROOT = new URL("./", import.meta.url).pathname; +const V2 = opt("--set") === "v2"; +if (opt("--set") && !V2 && opt("--set") !== "v1") { console.error(`unknown --set ${opt("--set")} (v1 | v2)`); process.exit(2); } +const SETUSED = V2 ? SET_V2 : SET; +const OUT = V2 ? "deployments/v2" : "deployments"; +const OUTFORK = V2 ? "deployments/fork-v2" : "deployments/fork"; const ANVIL = process.env.ANVIL_BIN || (existsSync(`${process.env.HOME}/.foundry/bin/anvil`) ? `${process.env.HOME}/.foundry/bin/anvil` : "anvil"); const ERC8004 = { IdentityRegistry: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", @@ -111,9 +123,16 @@ async function main() { const liveUrl = await pickRpc(chain, opt("--rpc")); const live = providerFor(liveUrl, chain.chainId); - const deployer = loadDeployer(live); - const plan = resolvePlan(deployer.address); - log(`deployer ${deployer.address} rpc ${liveUrl}`); + // v1: the one deployer key. v2: roles from config (real runs) or anvil accounts (fork); a v2 key is only loaded to broadcast. + const roles = V2 ? (mode === "fork" ? forkRoles() : realRoles()) : null; + let deployer; + if (V2) deployer = mode === "broadcast" ? loadDeployer(live, v2KeyFile(opt("--key-file"))) : { address: roles.deployer }; + else deployer = loadDeployer(live); + if (V2 && mode === "broadcast" && deployer.address !== roles.deployer) throw new Error(`--key-file address ${deployer.address} != config deployer ${roles.deployer}`); + const plan = V2 + ? resolvePlan(deployer.address, { owner: roles.owner, operator: roles.operator, ...chainParams(chain.name) }, SET_V2) + : resolvePlan(deployer.address); + log(`deployer ${deployer.address} rpc ${liveUrl}${V2 ? ` (v2: owner ${roles.owner}, operator ${roles.operator})` : ""}`); for (const s of plan) log(` nonce ${s.nonce} ${s.name.padEnd(22)} -> ${s.address}`); if (mode === "broadcast") { @@ -135,31 +154,34 @@ async function main() { } const need = (gas * price * 150n) / 100n; log(`\nnonce ${nonce}, balance ${formatEther(bal)} ${chain.native}; estimated gas ${gas} @ ${price} wei = ${formatEther(gas * price)} ${chain.native} (fund >= ${formatEther(need)} incl. 50% buffer; L2 data fees: see funding table)`); - if (nonce > 0 && (await live.getCode(plan[Math.min(nonce, plan.length) - 1].address)).length <= 2) + const base = plan[0].nonce, done = nonce - base; + if (done < 0 || done > plan.length) throw new Error(`deployer nonce ${nonce} on ${chain.name} is outside this set's nonces ${base}..${base + plan.length}: nonce desync.`); + if (done > 0 && (await live.getCode(plan[done - 1].address)).length <= 2) throw new Error(`deployer nonce ${nonce} on ${chain.name} but the contract for nonce ${nonce - 1} is missing: nonce desync.`); if (mode === "dry-run") { log("dry run only. Nothing sent."); return; } if (bal < (gas * price)) { console.error(`Balance too low. Fund ${deployer.address} with >= ${formatEther(need)} ${chain.native} on ${chain.name}.`); process.exit(4); } const results = await deploySet(live, deployer, plan, { log, gasLimitPct: chain.gasLimitPct }); - const smoke = await readOnly(live, plan, deployer.address); + const smoke = V2 ? await readOnlyV2(live, plan, roles) : await readOnly(live, plan, deployer.address); log(`smoke: ${smoke.length} checks passed`); const verification = {}; - mkdirSync(`${ROOT}deployments/verify`, { recursive: true }); + mkdirSync(`${ROOT}${OUT}/verify`, { recursive: true }); for (const s of plan) { - writeFileSync(`${ROOT}deployments/verify/${s.name}.json`, json(s.compiled.input)); + writeFileSync(`${ROOT}${OUT}/verify/${s.name}.json`, json(s.compiled.input)); const r = results.find((x) => x.name === s.name); verification[s.name] = flag("--no-verify") ? { ok: false, todo: "skipped (--no-verify)" } : await verifyStep(chain, s, r?.txHash); log(` verify ${s.name}: ${verification[s.name].ok ? "OK via " + verification[s.name].via : "TODO " + JSON.stringify(verification[s.name].tried ?? verification[s.name].todo).slice(0, 200)}`); } const record = { - chain: chain.name, chainId: chain.chainId, deployer: deployer.address, operator: plan[0].args[0], setVersion: SET.version, + chain: chain.name, chainId: chain.chainId, deployer: deployer.address, operator: V2 ? roles.operator : plan[0].args[0], ...(V2 ? { owner: roles.owner } : {}), setVersion: SETUSED.version, compiler: { solc: plan[0].compiled.compilerVersion, settings: { optimizer: SETTINGS.optimizer, evmVersion: SETTINGS.evmVersion } }, deployedAt: new Date().toISOString(), explorer: chain.explorer, contracts: Object.fromEntries(results.map(({ rawTx, ...r }) => [r.name, { ...r, verified: verification[r.name] }])), erc8004: await registries(live), smoke, legacy: chain.legacy ?? null, }; - writeFileSync(`${ROOT}deployments/${chain.name}.json`, json(record)); - log(`wrote deployments/${chain.name}.json`); + mkdirSync(`${ROOT}${OUT}`, { recursive: true }); + writeFileSync(`${ROOT}${OUT}/${chain.name}.json`, json(record)); + log(`wrote ${OUT}/${chain.name}.json`); return; } @@ -173,15 +195,24 @@ async function main() { const { url: forkUrl, child } = started; try { const fork = providerFor(forkUrl, chain.chainId); - const forkWallet = new Wallet(deployer.privateKey, fork); + // v1 forks with the real deployer key (it is only ever funded on the fork). v2 NEVER does: anvil account 0. + const anvil = V2 ? anvilWallets(fork) : null; + const forkWallet = V2 ? anvil[0] : new Wallet(deployer.privateKey, fork); + if (V2) for (const w of anvil) { await fork.send("anvil_setCode", [w.address, "0x"]); await fork.send("anvil_setNonce", [w.address, "0x0"]); await fork.send("anvil_setBalance", [w.address, "0x3635C9ADC5DEA00000"]); } // clean EOAs at nonce 0 (the public anvil keys are heavily used on mainnets), 1000 native, fork only + if (V2 && plan[0].nonce > 0) await fork.send("anvil_setNonce", [anvil[0].address, "0x" + plan[0].nonce.toString(16)]); // the fork deployer starts where the real one does (set nonceBase) const startNonce = await fork.getTransactionCount(deployer.address); await fork.send("anvil_setBalance", [deployer.address, "0x56BC75E2D63100000"]); // 100 native, fork only const t0 = Date.now(); const results = await deploySet(fork, forkWallet, plan, { log, gasLimitPct: chain.gasLimitPct }); - const smoke = await readOnly(fork, plan, deployer.address); + const smoke = V2 ? await readOnlyV2(fork, plan, roles) : await readOnly(fork, plan, deployer.address); log(`smoke: ${smoke.length} read-only checks passed`); - let steps = null; - if (!flag("--no-e2e")) { + let steps = null, attacks = null; + if (!flag("--no-e2e") && V2) { + const [, owner, op, client, worker, party] = anvil; + const r = await e2eV2(fork, plan, { owner, op, client, worker, party }, async (s) => { await fork.send("evm_increaseTime", [s]); await fork.send("evm_mine", []); }); + steps = r.steps; attacks = r.attacks; + log(`e2e: ${steps.length} honest-path txs passed; ${attacks.length} attack sequences blocked (H-01, H-02, H-03, M-03, Olympix 5.1.1/5.2.2, false pass/fail)`); + } else if (!flag("--no-e2e")) { // Fresh throwaway keys, funded on the fork only (the well-known anvil test accounts carry // EIP-7702 delegations on several mainnets, so they are not clean EOAs on a fork). const signers = [0, 1, 2].map(() => Wallet.createRandom().connect(fork)); @@ -206,12 +237,12 @@ async function main() { at: new Date().toISOString(), seconds: Math.round((Date.now() - t0) / 1000), deployer: deployer.address, startNonce, contracts: Object.fromEntries(results.map(({ rawTx, ...r }) => [r.name, r])), totals: { gasUsed: gasUsed.toString(), liveGasPriceWei: gasPrice.toString(), l1FeeWei: l1.toString(), l1FeeMethod: [...l1how].join("; "), costWei: costWei.toString(), cost: formatEther(costWei), native: chain.native }, - smoke: smoke.map((c) => c.check), e2e: steps, erc8004: await registries(live), + smoke: smoke.map((c) => c.check), e2e: steps, ...(V2 ? { attacksBlocked: attacks, setVersion: SETUSED.version } : {}), erc8004: await registries(live), }; - mkdirSync(`${ROOT}deployments/fork`, { recursive: true }); - writeFileSync(`${ROOT}deployments/fork/${chain.name}.json`, json(record)); + mkdirSync(`${ROOT}${OUTFORK}`, { recursive: true }); + writeFileSync(`${ROOT}${OUTFORK}/${chain.name}.json`, json(record)); log(`fork total: gas ${gasUsed} @ live ${gasPrice} wei + L1 ${formatEther(l1)} = ${formatEther(costWei)} ${chain.native}`); - log(`wrote deployments/fork/${chain.name}.json`); + log(`wrote ${OUTFORK}/${chain.name}.json`); } finally { if (!flag("--keep-anvil")) child.kill(); } diff --git a/deployments/fork-v2/SUMMARY.json b/deployments/fork-v2/SUMMARY.json new file mode 100644 index 0000000..5050618 --- /dev/null +++ b/deployments/fork-v2/SUMMARY.json @@ -0,0 +1,182 @@ +{ + "ethereum": { + "ok": true, + "seconds": 14, + "gasUsed": "6475274", + "cost": "0.00035636084446703", + "native": "ETH", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "base": { + "ok": true, + "seconds": 51, + "gasUsed": "6449676", + "cost": "0.000038898268888678", + "native": "ETH", + "e2eTxs": 42, + "smokeChecks": 28 + }, + "bnb": { + "ok": true, + "seconds": 11, + "gasUsed": "6475274", + "cost": "0.0003237637", + "native": "BNB", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "celo": { + "ok": true, + "seconds": 18, + "gasUsed": "6475322", + "cost": "1.311252705", + "native": "CELO", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "polygon": { + "ok": true, + "seconds": 12, + "gasUsed": "6475322", + "cost": "1.60429179856125992", + "native": "POL", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "optimism": { + "ok": true, + "seconds": 12, + "gasUsed": "6475274", + "cost": "0.000006712887343092", + "native": "ETH", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "arbitrum": { + "ok": true, + "seconds": 17, + "gasUsed": "6475274", + "cost": "0.00013020417144", + "native": "ETH", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "avalanche": { + "ok": true, + "seconds": 9, + "gasUsed": "6475298", + "cost": "0.0161882459712947", + "native": "AVAX", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "gnosis": { + "ok": true, + "seconds": 15, + "gasUsed": "6475298", + "cost": "0.00000000006475298", + "native": "xDAI", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "linea": { + "ok": true, + "seconds": 22, + "gasUsed": "6475274", + "cost": "0.000320988630673464", + "native": "ETH", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "scroll": { + "ok": true, + "seconds": 13, + "gasUsed": "6475274", + "cost": "0.00009141459618385", + "native": "ETH", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "unichain": { + "ok": true, + "seconds": 15, + "gasUsed": "6475274", + "cost": "0.000009789265857029", + "native": "ETH", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "sonic": { + "ok": true, + "seconds": 11, + "gasUsed": "6475322", + "cost": "0.35614271", + "native": "S", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "mantle": { + "ok": true, + "seconds": 30, + "gasUsed": "6475298", + "cost": "0.323768321839730786", + "native": "MNT", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "arc": { + "ok": true, + "seconds": 54, + "gasUsed": "6449700", + "cost": "0.15414783", + "native": "USDC", + "e2eTxs": 42, + "smokeChecks": 28 + }, + "monad": { + "ok": true, + "seconds": 9, + "gasUsed": "6452209", + "cost": "0.658125318", + "native": "MON", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "robinhood": { + "ok": true, + "seconds": 16, + "gasUsed": "6475274", + "cost": "0.000129609084384", + "native": "ETH", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "rootstock": { + "ok": true, + "seconds": 15, + "gasUsed": "6475274", + "cost": "0.0001687819019744", + "native": "RBTC", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "plasma": { + "ok": true, + "seconds": 19, + "gasUsed": "6475322", + "cost": "0.000064128338476356", + "native": "XPL", + "e2eTxs": 0, + "smokeChecks": 28 + }, + "stable": { + "ok": true, + "seconds": 13, + "gasUsed": "6475298", + "cost": "0.00728471025", + "native": "USDT0", + "e2eTxs": 0, + "smokeChecks": 28 + } +} diff --git a/deployments/fork-v2/arbitrum.json b/deployments/fork-v2/arbitrum.json new file mode 100644 index 0000000..49fcc33 --- /dev/null +++ b/deployments/fork-v2/arbitrum.json @@ -0,0 +1,171 @@ +{ + "chain": "arbitrum", + "chainId": 42161, + "mode": "anvil-fork", + "forkedFrom": "https://arb1.arbitrum.io/rpc", + "forkBlock": 516424009, + "at": "2026-10-19T23:05:42.303Z", + "seconds": 10, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0xbd7385ace83556e75ffe148f706c1f68e9a29c5417eb686d1124164bcec60057", + "block": 516423970, + "gasUsed": "1162822", + "effectiveGasPrice": "1017500001", + "l1FeeWei": "90954360000" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x251ccca2b174d975d1aa51e7cca32c1bb3a42101be9f5bd28ee1de2db2621d86", + "block": 516423971, + "gasUsed": "904910", + "effectiveGasPrice": "1015312501", + "l1FeeWei": "70159068000" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "2000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0x7a8052b5d71ea4fea9d5f3b04e015778a032742d1a2430e74ee3141ea6155d76", + "block": 516423972, + "gasUsed": "1711521", + "effectiveGasPrice": "1013398439", + "l1FeeWei": "117459342000" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xedc0ba4b90abbfbe54a105b9a9e780c06a16d528e0f1855dfcaca9f63b29522d", + "block": 516423973, + "gasUsed": "369083", + "effectiveGasPrice": "1011723635", + "l1FeeWei": "33016032000" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x9f1188d533590ee95f1f2244a507e1e2103f1beabed58b30368997118454600a", + "block": 516423974, + "gasUsed": "408763", + "effectiveGasPrice": "1010258181", + "l1FeeWei": "37603818000" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "2000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0xd393fb8251b9531a0165d068accbccdd310c67e2cf413fa3fa054b7f8435668e", + "block": 516423975, + "gasUsed": "1918175", + "effectiveGasPrice": "1008975909", + "l1FeeWei": "129339504000" + } + }, + "totals": { + "gasUsed": "6475274", + "liveGasPriceWei": "20034000", + "l1FeeWei": "478532124000", + "l1FeeMethod": "arbitrum NodeInterface: 4540 L1 gas units; arbitrum NodeInterface: 3502 L1 gas units; arbitrum NodeInterface: 5863 L1 gas units; arbitrum NodeInterface: 1648 L1 gas units; arbitrum NodeInterface: 1877 L1 gas units; arbitrum NodeInterface: 6456 L1 gas units", + "costWei": "130204171440000", + "cost": "0.00013020417144", + "native": "ETH" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/arc.json b/deployments/fork-v2/arc.json new file mode 100644 index 0000000..ad1c363 --- /dev/null +++ b/deployments/fork-v2/arc.json @@ -0,0 +1,434 @@ +{ + "chain": "arc", + "chainId": 5042, + "mode": "anvil-fork", + "forkedFrom": "https://rpc.mainnet.arc.io", + "forkBlock": 27514359, + "at": "2026-10-23T21:01:39.793Z", + "seconds": 51, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 6, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xAfF33F45707ebd84C779D2D0836513ef6706312c", + "nonce": 6, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "188c8c4f4d75ca748cad006a4b30a825d2bc9e9725e42085685000e3ee0232dd", + "txHash": "0x83de1869cda9efd2e8e906797dc727d552c0a19857e53a85136b2382bc7107c7", + "block": 27514255, + "gasUsed": "1167799", + "effectiveGasPrice": "18546384000", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x9e4A48A5Ebc237f6AEaF71211DcdF75628fC0507", + "nonce": 7, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "c83bd1c9ce4e8643b39246586619b7a12e203653b3f03386de03ab58b58c4a9a", + "txHash": "0x56007fd54cfd2ecd647843c06c7bfb46aac90b7b96c6bba6e06ff9bc88e52b52", + "block": 27514256, + "gasUsed": "920066", + "effectiveGasPrice": "16523841415", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x6bfe9aE3C60C764B88D77218faeF7E4C87EEF547", + "nonce": 8, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x9e4A48A5Ebc237f6AEaF71211DcdF75628fC0507", + "5000000000000000000", + "0" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000009e4a48a5ebc237f6aeaf71211dcdf75628fc05070000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "0b84858a48a63548f0b889ca451587c6e57c81cf3b239e6521188f58aa8c694d", + "txHash": "0x38c568a459cc259c1a55536ea4deea1106279278377ad9d43a44221e48259155", + "block": 27514257, + "gasUsed": "1453368", + "effectiveGasPrice": "14702385894", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xe7de9717cB95634639eF114Cda060dB1Fdf60F16", + "nonce": 9, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xa9bee3dab4e3656dfc8f2092537cb8194b179d5626ab7d49f4e8fc089dc6c66a", + "block": 27514258, + "gasUsed": "369071", + "effectiveGasPrice": "13155542734", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x78461d287398a45a1204EfbcCa88804f9dacfA87", + "nonce": 10, + "args": [ + "0x37a722A40875ff6a26E1F506B96a8d1Ae08353a1" + ], + "constructorArgs": "0x00000000000000000000000037a722a40875ff6a26e1f506b96a8d1ae08353a1", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x6104ce5b2904386916b2c98ee4da9e2bd7a2a2933a6dd5826b5e707866a1c119", + "block": 27514259, + "gasUsed": "408763", + "effectiveGasPrice": "11673485379", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x37a722A40875ff6a26E1F506B96a8d1Ae08353a1", + "nonce": 11, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x78461d287398a45a1204EfbcCa88804f9dacfA87", + "86400", + "5000000000000000000", + "0" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000078461d287398a45a1204efbcca88804f9dacfa8700000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "b5dc07973805879660216a2047ce1471f6a05abeb49c07466efc77e0c5f56bcf", + "txHash": "0x99e4b4c6ff02869284724e8a5c4a9f593dd0dbf937fc3c52611ec42b97106bcd", + "block": 27514260, + "gasUsed": "1930219", + "effectiveGasPrice": "10375657423", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6249286", + "liveGasPriceWei": "25000000000", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "156232150000000000", + "cost": "0.15623215", + "native": "USDC" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.verdictFor(no job) == 0", + "bond has no challenge() (v2.1: false verdicts are refused, not slashed)", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated", + "bond.specHashFor is domain separated" + ], + "e2e": [ + { + "step": "registry.validationRequest", + "gasUsed": 123282 + }, + { + "step": "job.createJob #1", + "gasUsed": 203597 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 165934 + }, + { + "step": "job.submit (bytes, sha256 on-chain)", + "gasUsed": 55445 + }, + { + "step": "bond.recordScore(100) completes the job", + "gasUsed": 221752 + }, + { + "step": "registry.validationResponse", + "gasUsed": 132382 + }, + { + "step": "job.withdraw (provider pulls payout)", + "gasUsed": 32248 + }, + { + "step": "settlement.payForRoute", + "gasUsed": 25567 + }, + { + "step": "settlement.withdraw (owner)", + "gasUsed": 32279 + }, + { + "step": "job.createJob #2", + "gasUsed": 186485 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 165922 + }, + { + "step": "job.createJob #3", + "gasUsed": 169397 + }, + { + "step": "job.submit (j3, wrong output)", + "gasUsed": 54547 + }, + { + "step": "job.createJob #4", + "gasUsed": 169397 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 148822 + }, + { + "step": "job.submit (j4, provider submits the public commitment)", + "gasUsed": 54535 + }, + { + "step": "bond.recordScore(0) honest fail", + "gasUsed": 197054 + }, + { + "step": "job.createJob #5", + "gasUsed": 169397 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 148822 + }, + { + "step": "job.submit (j5, wrong output bytes)", + "gasUsed": 55205 + }, + { + "step": "bond.recordScore(0) on wrong bytes", + "gasUsed": 162866 + }, + { + "step": "job.createJob #7", + "gasUsed": 169397 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 148834 + }, + { + "step": "job.submit (j7, leaked expected, no bytes)", + "gasUsed": 54547 + }, + { + "step": "bond.recordScore(0) j7", + "gasUsed": 162842 + }, + { + "step": "job.createJob #8", + "gasUsed": 169397 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 148834 + }, + { + "step": "job.submit (j8, correct bytes)", + "gasUsed": 55445 + }, + { + "step": "bond.recordScore(100) j8", + "gasUsed": 180028 + }, + { + "step": "job.createJob #6", + "gasUsed": 169397 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 148834 + }, + { + "step": "job.submit (j6, bytes, operator stays silent)", + "gasUsed": 55445 + }, + { + "step": "job.claimRefund (expired, never submitted)", + "gasUsed": 47142 + }, + { + "step": "bond.release (no verdict was given)", + "gasUsed": 69800 + }, + { + "step": "job.claimRefund (j6, silent evaluator)", + "gasUsed": 47292 + }, + { + "step": "bond.release (j6, forfeited to the provider)", + "gasUsed": 81906 + }, + { + "step": "bond.withdraw (provider gets the forfeited bond)", + "gasUsed": 35571 + }, + { + "step": "bond.withdraw (staker)", + "gasUsed": 32297 + }, + { + "step": "mock.recordPayment", + "gasUsed": 138491 + }, + { + "step": "arbiter.fileEvidence (payer)", + "gasUsed": 60606 + }, + { + "step": "arbiter.rule(Refund)", + "gasUsed": 250666 + }, + { + "step": "arbiter.dispute (harmed payee)", + "gasUsed": 59516 + }, + { + "step": "arbiter.resolve(Release) overturns", + "gasUsed": 86634 + }, + { + "step": "arbiter.withdraw (payee gets the operator bond)", + "gasUsed": 32316 + }, + { + "step": "mock.recordPayment", + "gasUsed": 121391 + }, + { + "step": "arbiter.fileEvidence (payer, p2)", + "gasUsed": 60606 + }, + { + "step": "arbiter.rule(Refund, p2)", + "gasUsed": 233566 + }, + { + "step": "arbiter.finalize (undisputed, after window)", + "gasUsed": 130353 + }, + { + "step": "arbiter.withdraw (operator bond back)", + "gasUsed": 32316 + } + ], + "attacksBlocked": [ + { + "attack": "Olympix 5.1.1 operator commits a value the client did not fix", + "blockedBy": "CommitmentNotBound" + }, + { + "attack": "H-01 recordScore before the provider submits", + "blockedBy": "NotSubmitted" + }, + { + "attack": "H-01 bond back without a verdict", + "blockedBy": "JobNotExpired" + }, + { + "attack": "H-02 commit after submission", + "blockedBy": "NotOpen" + }, + { + "attack": "H-02 pass on a copied public value", + "blockedBy": "VerdictContradictsEvidence" + }, + { + "attack": "false pass on wrong on-chain bytes", + "blockedBy": "VerdictContradictsEvidence" + }, + { + "attack": "Olympix 5.1.1 pass on a leaked hash without bytes", + "blockedBy": "VerdictContradictsEvidence" + }, + { + "attack": "false fail on correct on-chain bytes", + "blockedBy": "VerdictContradictsEvidence" + }, + { + "attack": "late submit after expiry", + "blockedBy": "PastDeadline" + }, + { + "attack": "Olympix 5.2.2 silence on delivered work is free", + "blockedBy": "bond forfeited to provider" + }, + { + "attack": "H-03 stranger files evidence for someone else's payment", + "blockedBy": "NotAParty" + }, + { + "attack": "H-03 operator cites a non-party", + "blockedBy": "NotAParty" + }, + { + "attack": "H-03 refund does not execute inside rule()", + "blockedBy": "optimistic window" + }, + { + "attack": "H-03 finalize inside the window", + "blockedBy": "WindowOpen" + }, + { + "attack": "non-owner setOperator", + "blockedBy": "NotOwner" + }, + { + "attack": "non-owner transferOwnership", + "blockedBy": "NotOwner" + } + ], + "setVersion": "2026-10-23-v2.1", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/avalanche.json b/deployments/fork-v2/avalanche.json new file mode 100644 index 0000000..d8aa3ba --- /dev/null +++ b/deployments/fork-v2/avalanche.json @@ -0,0 +1,171 @@ +{ + "chain": "avalanche", + "chainId": 43114, + "mode": "anvil-fork", + "forkedFrom": "https://api.avax.network/ext/bc/C/rpc", + "forkBlock": 98008349, + "at": "2026-10-19T23:05:52.295Z", + "seconds": 4, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0xfeb3f267f925fdb785e776b2492eb0c5585a85cfaf6c29825fc4c74bec8c15b1", + "block": 98008346, + "gasUsed": "1162822", + "effectiveGasPrice": "3193802204", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x896d7c8b881466d418866ac865a666350a603ea00a101868a7b838a9244d7082", + "block": 98008347, + "gasUsed": "904910", + "effectiveGasPrice": "2927548809", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "500000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000006f05b59d3b200000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0x6eaabf5321d5f43b7c57c72039ac370ac1198be393896d290cba54e8336a3045", + "block": 98008348, + "gasUsed": "1711533", + "effectiveGasPrice": "2692056015", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x2630a991af27e98cf1bc5694e4bec90e1c577d1d4999b1434e9a77a6b4bf64a0", + "block": 98008349, + "gasUsed": "369083", + "effectiveGasPrice": "2489599044", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0xc2118a452ade4e16ccd74ed00a06f9c18589ed798df205b014f6b73b956310b3", + "block": 98008350, + "gasUsed": "408763", + "effectiveGasPrice": "2305117244", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "500000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x5da15ab5f88b34c9dc5f01eaeb9645f9047233a99aa273b5b452d26061e3a426", + "block": 98008351, + "gasUsed": "1918187", + "effectiveGasPrice": "2143644725", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475298", + "liveGasPriceWei": "2500000150", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "16188245971294700", + "cost": "0.0161882459712947", + "native": "AVAX" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/base.json b/deployments/fork-v2/base.json new file mode 100644 index 0000000..3b75440 --- /dev/null +++ b/deployments/fork-v2/base.json @@ -0,0 +1,434 @@ +{ + "chain": "base", + "chainId": 8453, + "mode": "anvil-fork", + "forkedFrom": "https://base-rpc.publicnode.com", + "forkBlock": 52999945, + "at": "2026-10-23T21:00:38.712Z", + "seconds": 50, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 6, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xAfF33F45707ebd84C779D2D0836513ef6706312c", + "nonce": 6, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "188c8c4f4d75ca748cad006a4b30a825d2bc9e9725e42085685000e3ee0232dd", + "txHash": "0x7b273f1f7a180986086e17138b8d403e3f74de3748f9e69425e5cb3424bb5918", + "block": 52999921, + "gasUsed": "1167799", + "effectiveGasPrice": "1004993460", + "l1FeeWei": "19358672360" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x9e4A48A5Ebc237f6AEaF71211DcdF75628fC0507", + "nonce": 7, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "c83bd1c9ce4e8643b39246586619b7a12e203653b3f03386de03ab58b58c4a9a", + "txHash": "0x67310cf91c27370c6e0cbf5fc591552dfc5eac1c2fa69b35fef675c6de13feab", + "block": 52999922, + "gasUsed": "920066", + "effectiveGasPrice": "1004973837", + "l1FeeWei": "14742746611" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x6bfe9aE3C60C764B88D77218faeF7E4C87EEF547", + "nonce": 8, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x9e4A48A5Ebc237f6AEaF71211DcdF75628fC0507", + "2000000000000000", + "0" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000009e4a48a5ebc237f6aeaf71211dcdf75628fc050700000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "0b84858a48a63548f0b889ca451587c6e57c81cf3b239e6521188f58aa8c694d", + "txHash": "0x588b3ab2208798839e21355967b0549ae7aa4b07d464da2295a02a5db590ab45", + "block": 52999923, + "gasUsed": "1453356", + "effectiveGasPrice": "1004954217", + "l1FeeWei": "21646866815" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xe7de9717cB95634639eF114Cda060dB1Fdf60F16", + "nonce": 9, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x7e94bce39db26116e1ece8bbf716d9d4eee03d8876607888409c72646ee7f4dd", + "block": 52999924, + "gasUsed": "369071", + "effectiveGasPrice": "1004934833", + "l1FeeWei": "6296689754" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x78461d287398a45a1204EfbcCa88804f9dacfA87", + "nonce": 10, + "args": [ + "0x37a722A40875ff6a26E1F506B96a8d1Ae08353a1" + ], + "constructorArgs": "0x00000000000000000000000037a722a40875ff6a26e1f506b96a8d1ae08353a1", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x533c5d0dabc3f3d05234bd7918ec08693863475ac208545076512a29e9ff243f", + "block": 52999925, + "gasUsed": "408763", + "effectiveGasPrice": "1004915203", + "l1FeeWei": "7453142244" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x37a722A40875ff6a26E1F506B96a8d1Ae08353a1", + "nonce": 11, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x78461d287398a45a1204EfbcCa88804f9dacfA87", + "86400", + "2000000000000000", + "0" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000078461d287398a45a1204efbcca88804f9dacfa87000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "b5dc07973805879660216a2047ce1471f6a05abeb49c07466efc77e0c5f56bcf", + "txHash": "0x1266241e7f57d4a39d3cdaddef4e0079383af4d2bff7b4017f9a9134902f0016", + "block": 52999926, + "gasUsed": "1930207", + "effectiveGasPrice": "1004895663", + "l1FeeWei": "28437318613" + } + }, + "totals": { + "gasUsed": "6249262", + "liveGasPriceWei": "6000000", + "l1FeeWei": "97935436397", + "l1FeeMethod": "op-stack oracle getL1Fee", + "costWei": "37593507436397", + "cost": "0.000037593507436397", + "native": "ETH" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.verdictFor(no job) == 0", + "bond has no challenge() (v2.1: false verdicts are refused, not slashed)", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated", + "bond.specHashFor is domain separated" + ], + "e2e": [ + { + "step": "registry.validationRequest", + "gasUsed": 123282 + }, + { + "step": "job.createJob #1", + "gasUsed": 203597 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 165922 + }, + { + "step": "job.submit (bytes, sha256 on-chain)", + "gasUsed": 55445 + }, + { + "step": "bond.recordScore(100) completes the job", + "gasUsed": 221752 + }, + { + "step": "registry.validationResponse", + "gasUsed": 132382 + }, + { + "step": "job.withdraw (provider pulls payout)", + "gasUsed": 32248 + }, + { + "step": "settlement.payForRoute", + "gasUsed": 25567 + }, + { + "step": "settlement.withdraw (owner)", + "gasUsed": 32279 + }, + { + "step": "job.createJob #2", + "gasUsed": 186497 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 165934 + }, + { + "step": "job.createJob #3", + "gasUsed": 169397 + }, + { + "step": "job.submit (j3, wrong output)", + "gasUsed": 54547 + }, + { + "step": "job.createJob #4", + "gasUsed": 169397 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 148834 + }, + { + "step": "job.submit (j4, provider submits the public commitment)", + "gasUsed": 54547 + }, + { + "step": "bond.recordScore(0) honest fail", + "gasUsed": 197054 + }, + { + "step": "job.createJob #5", + "gasUsed": 169397 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 148834 + }, + { + "step": "job.submit (j5, wrong output bytes)", + "gasUsed": 55205 + }, + { + "step": "bond.recordScore(0) on wrong bytes", + "gasUsed": 162854 + }, + { + "step": "job.createJob #7", + "gasUsed": 169397 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 148834 + }, + { + "step": "job.submit (j7, leaked expected, no bytes)", + "gasUsed": 54547 + }, + { + "step": "bond.recordScore(0) j7", + "gasUsed": 162854 + }, + { + "step": "job.createJob #8", + "gasUsed": 169385 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 148822 + }, + { + "step": "job.submit (j8, correct bytes)", + "gasUsed": 55445 + }, + { + "step": "bond.recordScore(100) j8", + "gasUsed": 180028 + }, + { + "step": "job.createJob #6", + "gasUsed": 169397 + }, + { + "step": "bond.stakeAndCommit", + "gasUsed": 148834 + }, + { + "step": "job.submit (j6, bytes, operator stays silent)", + "gasUsed": 55445 + }, + { + "step": "job.claimRefund (expired, never submitted)", + "gasUsed": 47142 + }, + { + "step": "bond.release (no verdict was given)", + "gasUsed": 69812 + }, + { + "step": "job.claimRefund (j6, silent evaluator)", + "gasUsed": 47292 + }, + { + "step": "bond.release (j6, forfeited to the provider)", + "gasUsed": 81906 + }, + { + "step": "bond.withdraw (provider gets the forfeited bond)", + "gasUsed": 35571 + }, + { + "step": "bond.withdraw (staker)", + "gasUsed": 32297 + }, + { + "step": "mock.recordPayment", + "gasUsed": 138491 + }, + { + "step": "arbiter.fileEvidence (payer)", + "gasUsed": 60606 + }, + { + "step": "arbiter.rule(Refund)", + "gasUsed": 250666 + }, + { + "step": "arbiter.dispute (harmed payee)", + "gasUsed": 59516 + }, + { + "step": "arbiter.resolve(Release) overturns", + "gasUsed": 86634 + }, + { + "step": "arbiter.withdraw (payee gets the operator bond)", + "gasUsed": 32316 + }, + { + "step": "mock.recordPayment", + "gasUsed": 121391 + }, + { + "step": "arbiter.fileEvidence (payer, p2)", + "gasUsed": 60606 + }, + { + "step": "arbiter.rule(Refund, p2)", + "gasUsed": 233566 + }, + { + "step": "arbiter.finalize (undisputed, after window)", + "gasUsed": 130353 + }, + { + "step": "arbiter.withdraw (operator bond back)", + "gasUsed": 32316 + } + ], + "attacksBlocked": [ + { + "attack": "Olympix 5.1.1 operator commits a value the client did not fix", + "blockedBy": "CommitmentNotBound" + }, + { + "attack": "H-01 recordScore before the provider submits", + "blockedBy": "NotSubmitted" + }, + { + "attack": "H-01 bond back without a verdict", + "blockedBy": "JobNotExpired" + }, + { + "attack": "H-02 commit after submission", + "blockedBy": "NotOpen" + }, + { + "attack": "H-02 pass on a copied public value", + "blockedBy": "VerdictContradictsEvidence" + }, + { + "attack": "false pass on wrong on-chain bytes", + "blockedBy": "VerdictContradictsEvidence" + }, + { + "attack": "Olympix 5.1.1 pass on a leaked hash without bytes", + "blockedBy": "VerdictContradictsEvidence" + }, + { + "attack": "false fail on correct on-chain bytes", + "blockedBy": "VerdictContradictsEvidence" + }, + { + "attack": "late submit after expiry", + "blockedBy": "PastDeadline" + }, + { + "attack": "Olympix 5.2.2 silence on delivered work is free", + "blockedBy": "bond forfeited to provider" + }, + { + "attack": "H-03 stranger files evidence for someone else's payment", + "blockedBy": "NotAParty" + }, + { + "attack": "H-03 operator cites a non-party", + "blockedBy": "NotAParty" + }, + { + "attack": "H-03 refund does not execute inside rule()", + "blockedBy": "optimistic window" + }, + { + "attack": "H-03 finalize inside the window", + "blockedBy": "WindowOpen" + }, + { + "attack": "non-owner setOperator", + "blockedBy": "NotOwner" + }, + { + "attack": "non-owner transferOwnership", + "blockedBy": "NotOwner" + } + ], + "setVersion": "2026-10-23-v2.1", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/bnb.json b/deployments/fork-v2/bnb.json new file mode 100644 index 0000000..358b204 --- /dev/null +++ b/deployments/fork-v2/bnb.json @@ -0,0 +1,171 @@ +{ + "chain": "bnb", + "chainId": 56, + "mode": "anvil-fork", + "forkedFrom": "https://bsc-dataseed.bnbchain.org", + "forkBlock": 128639054, + "at": "2026-10-19T23:04:43.890Z", + "seconds": 5, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0x03828af3b0868dddf7e65211140af3d2af04cab69bf95ea7730877636f012fbb", + "block": 128639041, + "gasUsed": "1162822", + "effectiveGasPrice": "1000000000", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0xce577da43d67b57086114c56d2b210a8a5a75ec5c68a159656386c17377063e9", + "block": 128639042, + "gasUsed": "904910", + "effectiveGasPrice": "1000000000", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "6000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e6160000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000001550f7dca700000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0xbb9628fd1ef9131a653f4867dc6ccd67609ebe4e9a1f39ff1973f2f0e3a13c5f", + "block": 128639043, + "gasUsed": "1711521", + "effectiveGasPrice": "1000000000", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x5f5b5db5a8a80a595c617bc2b1ab02c21356e0c35f56bfcbfd1ceeb5e0752fb5", + "block": 128639044, + "gasUsed": "369083", + "effectiveGasPrice": "1000000000", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x3311bb83b7fbf0581ab992304c48719ec4a4469f3d70a7337126b468fe915207", + "block": 128639045, + "gasUsed": "408763", + "effectiveGasPrice": "1000000000", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "6000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a052268370000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000001550f7dca7000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x5158e42da393afb55dff617ddfd41a968b5c68d4c372aaf84d9ee52cfa5d8c37", + "block": 128639046, + "gasUsed": "1918175", + "effectiveGasPrice": "1000000000", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475274", + "liveGasPriceWei": "50000000", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "323763700000000", + "cost": "0.0003237637", + "native": "BNB" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/celo.json b/deployments/fork-v2/celo.json new file mode 100644 index 0000000..186bd56 --- /dev/null +++ b/deployments/fork-v2/celo.json @@ -0,0 +1,171 @@ +{ + "chain": "celo", + "chainId": 42220, + "mode": "anvil-fork", + "forkedFrom": "https://forno.celo.org", + "forkBlock": 80550342, + "at": "2026-10-19T23:05:01.789Z", + "seconds": 12, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0xf203c794837a8e7dbf4beb73bf258e10d0ffd8c5a1c33d8416ea583bc7629d38", + "block": 80550329, + "gasUsed": "1162822", + "effectiveGasPrice": "176543118334", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x56b93ca7320e27aa1986ecf4d8a7769a9502c8659eee4a76da86287081f33151", + "block": 80550330, + "gasUsed": "904910", + "effectiveGasPrice": "156301273542", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "60000000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000340aad21b3b7000000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0x05e256000a4bae4beaa0752579414e2563930a9c068cbb7efb6da3c5e9a4c063", + "block": 80550331, + "gasUsed": "1711545", + "effectiveGasPrice": "138059728312", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xbaf3a76839ad6a9d696fe14f326327b9cb6349cfb371f9f993218fc1dfd1d162", + "block": 80550332, + "gasUsed": "369083", + "effectiveGasPrice": "122882128046", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x384deb5d765a8712f93bfc5578469b0068993f12f8472d20819e82a4ad5ce6d1", + "block": 80550333, + "gasUsed": "408763", + "effectiveGasPrice": "108021733886", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "60000000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000340aad21b3b70000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x0bc70ef96c9e094a042d33e8dfd8b8c37cd40963bf48fe1d072f713d01ffdd7d", + "block": 80550334, + "gasUsed": "1918199", + "effectiveGasPrice": "95008571526", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475322", + "liveGasPriceWei": "202500000000", + "l1FeeWei": "0", + "l1FeeMethod": "op-stack oracle getL1Fee", + "costWei": "1311252705000000000", + "cost": "1.311252705", + "native": "CELO" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/ethereum.json b/deployments/fork-v2/ethereum.json new file mode 100644 index 0000000..7e19c49 --- /dev/null +++ b/deployments/fork-v2/ethereum.json @@ -0,0 +1,171 @@ +{ + "chain": "ethereum", + "chainId": 1, + "mode": "anvil-fork", + "forkedFrom": "https://ethereum-rpc.publicnode.com", + "forkBlock": 26229787, + "at": "2026-10-19T23:04:21.488Z", + "seconds": 8, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0x7ebe04deee5d15ad6114b0770aef207543b9c921c1c17edf22ce38079412917d", + "block": 26229787, + "gasUsed": "1162822", + "effectiveGasPrice": "1055031365", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x6081766c27453676430f79e7d76df1784738ee60b546c76bbc5a7fa05dedc2e7", + "block": 26229788, + "gasUsed": "904910", + "effectiveGasPrice": "1048419077", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "2000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0x317ebb3138d182bc336b636a613ec9a09f32153604b6fb3478f3eb6d5d0bb4f9", + "block": 26229789, + "gasUsed": "1711521", + "effectiveGasPrice": "1042549255", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xb189110e698f908a43b148cadaeb039eb9c41106bf2d48ae7ea34fe9b0fc03aa", + "block": 26229790, + "gasUsed": "369083", + "effectiveGasPrice": "1037534032", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x3d4d1e977240daa14643d5e7d4e0b7a03bed4aa5c759aed79b67993f1cd02c3f", + "block": 26229791, + "gasUsed": "408763", + "effectiveGasPrice": "1032900000", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "2000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0xd4d1ff30f631d222a1741486e291cea34ca67331ca11e5e13c468194fe8cb038", + "block": 26229792, + "gasUsed": "1918175", + "effectiveGasPrice": "1028843535", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475274", + "liveGasPriceWei": "55034095", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "356360844467030", + "cost": "0.00035636084446703", + "native": "ETH" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/gnosis.json b/deployments/fork-v2/gnosis.json new file mode 100644 index 0000000..aa3d565 --- /dev/null +++ b/deployments/fork-v2/gnosis.json @@ -0,0 +1,171 @@ +{ + "chain": "gnosis", + "chainId": 100, + "mode": "anvil-fork", + "forkedFrom": "https://rpc.gnosischain.com", + "forkBlock": 48844239, + "at": "2026-10-19T23:06:07.423Z", + "seconds": 9, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0x290ac8de063f7e8353a9dfcd1082f2040f3b548d9c94aeb9361e4b34fc986733", + "block": 48844238, + "gasUsed": "1162822", + "effectiveGasPrice": "1000000009", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x593babc7886d326a0275d7c3fb9b9235a2b30289082464556a62f94dcd130c1e", + "block": 48844239, + "gasUsed": "904910", + "effectiveGasPrice": "1000000009", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "5000000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e61600000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0xd12db0616ff834aecd03a46b60b6fd3ec1a12bf25969f621344c87fa27feaa46", + "block": 48844240, + "gasUsed": "1711533", + "effectiveGasPrice": "1000000008", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x8a355ba2654b527dfdbe9ebc2758ab511c82d4712140f196892ba31a8a51f9de", + "block": 48844241, + "gasUsed": "369083", + "effectiveGasPrice": "1000000008", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x270c4abfc7d5522ae76cc6b5bb0029e646b1bc644a51b88519e8d8742138caa8", + "block": 48844242, + "gasUsed": "408763", + "effectiveGasPrice": "1000000008", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "5000000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a0522683700000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000004563918244f4000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0xcea4172deb59a61eacea98a3ccb4661054d78d54f4ad58a375e1efe231f033b9", + "block": 48844243, + "gasUsed": "1918187", + "effectiveGasPrice": "1000000008", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475298", + "liveGasPriceWei": "10", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "64752980", + "cost": "0.00000000006475298", + "native": "xDAI" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/linea.json b/deployments/fork-v2/linea.json new file mode 100644 index 0000000..1390fd5 --- /dev/null +++ b/deployments/fork-v2/linea.json @@ -0,0 +1,171 @@ +{ + "chain": "linea", + "chainId": 59144, + "mode": "anvil-fork", + "forkedFrom": "https://rpc.linea.build", + "forkBlock": 32370743, + "at": "2026-10-19T23:06:28.854Z", + "seconds": 14, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0xf05d43f278f879a39a222c41c6dd54e544f7fe7bc74b80899ac5ea721b662475", + "block": 32370742, + "gasUsed": "1162822", + "effectiveGasPrice": "1000000007", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0xa3671b41395d35e186df7f95a15faad354a0ef325c4775305b834b47d2fe3ac9", + "block": 32370743, + "gasUsed": "904910", + "effectiveGasPrice": "1000000007", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "2000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0xdb6e6ac485aff4527b1c4bce6d30f0b383a32a8f1634715a51044cacc6439079", + "block": 32370744, + "gasUsed": "1711521", + "effectiveGasPrice": "1000000007", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x1c56d27c09c902760f3ba5ee3567f96ddb8560e9952eec53977c21056ebac2a9", + "block": 32370745, + "gasUsed": "369083", + "effectiveGasPrice": "1000000007", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0xae322a474d345a65be20465a2a514c73dee6aac69fdce1dc52af56f8bc8931bb", + "block": 32370746, + "gasUsed": "408763", + "effectiveGasPrice": "1000000007", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "2000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0xaaba931e05c41fc13303e8f94fcbfd4c0945274f041db404f47f5953022ff55a", + "block": 32370747, + "gasUsed": "1918175", + "effectiveGasPrice": "1000000007", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475274", + "liveGasPriceWei": "49571436", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "320988630673464", + "cost": "0.000320988630673464", + "native": "ETH" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/mantle.json b/deployments/fork-v2/mantle.json new file mode 100644 index 0000000..a04c005 --- /dev/null +++ b/deployments/fork-v2/mantle.json @@ -0,0 +1,171 @@ +{ + "chain": "mantle", + "chainId": 5000, + "mode": "anvil-fork", + "forkedFrom": "https://rpc.mantle.xyz", + "forkBlock": 102160470, + "at": "2026-10-19T23:07:35.071Z", + "seconds": 17, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0x1d4f34481c90b6ebef42eb88361715b4be71c91ab6456d35b5395c46f4dd128d", + "block": 102160460, + "gasUsed": "1162822", + "effectiveGasPrice": "44759645625", + "l1FeeWei": "531958727074" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x49907744e5df6323a45c5f9d9da48d28d3e49c4a0dfcdd8eccdc06ffae41629b", + "block": 102160461, + "gasUsed": "904910", + "effectiveGasPrice": "39501709417", + "l1FeeWei": "402478051110" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "9000000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e61600000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000007ce66c50e28400000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0x5131576440f506020c2a781e6542ff242a59dfa97017ec3373d10a0e7c990162", + "block": 102160462, + "gasUsed": "1711533", + "effectiveGasPrice": "34834164831", + "l1FeeWei": "686627547153" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xe3e75f6c4233cef641df066fad4667056314c23953dda718ec6176fa514b2fc8", + "block": 102160463, + "gasUsed": "369083", + "effectiveGasPrice": "30846178768", + "l1FeeWei": "173606319989" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x6b3c0aefb9474d43143413a62a2ee695fcd8c6f28bcde92b4d00379f01314b1a", + "block": 102160464, + "gasUsed": "408763", + "effectiveGasPrice": "27161305244", + "l1FeeWei": "205465918491" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "9000000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a0522683700000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000007ce66c50e284000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0xefa137d0b5ed38cc9db5d2c3a5359294eb86d377c07896254009c031e3d02ea4", + "block": 102160465, + "gasUsed": "1918187", + "effectiveGasPrice": "23935699479", + "l1FeeWei": "774173366969" + } + }, + "totals": { + "gasUsed": "6475298", + "liveGasPriceWei": "50000100000", + "l1FeeWei": "2774309930786", + "l1FeeMethod": "op-stack oracle getL1Fee", + "costWei": "323768321839730786", + "cost": "0.323768321839730786", + "native": "MNT" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/monad.json b/deployments/fork-v2/monad.json new file mode 100644 index 0000000..fd32123 --- /dev/null +++ b/deployments/fork-v2/monad.json @@ -0,0 +1,171 @@ +{ + "chain": "monad", + "chainId": 143, + "mode": "anvil-fork", + "forkedFrom": "https://rpc.monad.xyz", + "forkBlock": 114881940, + "at": "2026-10-19T23:07:57.259Z", + "seconds": 3, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0x096f64709c619ee83e505a08b31a042ea8721bc9316cf0d8e008ec0fdee6b192", + "block": 114881927, + "gasUsed": "1163394", + "effectiveGasPrice": "91657906334", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0xd023dcf593814464f19b2def2d708de99ef219edda1522231390ea26c2bc1c45", + "block": 114881928, + "gasUsed": "904573", + "effectiveGasPrice": "80501452817", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "200000000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000ad78ebc5ac62000000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0xcc0bdbddad04096f99ea24048b4e9417649412644ccf23d39c6f42f9aa441aeb", + "block": 114881929, + "gasUsed": "1697094", + "effectiveGasPrice": "70683629328", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xd1ce31ccfe8e48d611462d92ec7c3a8c835c6df04ad041f1317c24965e34d251", + "block": 114881930, + "gasUsed": "374875", + "effectiveGasPrice": "62170275111", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x39e283c6281c07d40fb671c0aa7a66ba80cfc1b8efd0d962f2b05d02f0c6315a", + "block": 114881931, + "gasUsed": "408628", + "effectiveGasPrice": "54562209401", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "200000000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000ad78ebc5ac620000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x831eb5241cccc795d62cddfb298ab6c4889ca44d409c80429e14c92e02d1cf30", + "block": 114881932, + "gasUsed": "1903645", + "effectiveGasPrice": "47903411591", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6452209", + "liveGasPriceWei": "102000000000", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "658125318000000000", + "cost": "0.658125318", + "native": "MON" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/optimism.json b/deployments/fork-v2/optimism.json new file mode 100644 index 0000000..97c522c --- /dev/null +++ b/deployments/fork-v2/optimism.json @@ -0,0 +1,171 @@ +{ + "chain": "optimism", + "chainId": 10, + "mode": "anvil-fork", + "forkedFrom": "https://optimism-rpc.publicnode.com", + "forkBlock": 158426174, + "at": "2026-10-19T23:05:25.788Z", + "seconds": 6, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0x03dd26c1cbeef37fe348758e20d1648e162d2304cc992bb7a4830fc3f4baf268", + "block": 158426171, + "gasUsed": "1162822", + "effectiveGasPrice": "1000015714", + "l1FeeWei": "26052427225" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x3f257b85f7d45deba0a08359c726c74930ee9f31ed20569bdc3a1211780ba607", + "block": 158426172, + "gasUsed": "904910", + "effectiveGasPrice": "1000015656", + "l1FeeWei": "19706299542" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "2000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0x48621fb7f28e67014d4f0674ab2030025cb13a20434829688cc88db7a23352d2", + "block": 158426173, + "gasUsed": "1711521", + "effectiveGasPrice": "1000015597", + "l1FeeWei": "33626407162" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xc8199c6e1b7b750ff70dcd21eda7149746bae07b08ef038cea82236360909de9", + "block": 158426174, + "gasUsed": "369083", + "effectiveGasPrice": "1000015540", + "l1FeeWei": "8488801503" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x7530b57d5c323d27f95f77009ec25e959a2caa5c5ce63cfbaca7a73b596710b2", + "block": 158426175, + "gasUsed": "408763", + "effectiveGasPrice": "1000015480", + "l1FeeWei": "10050309261" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "2000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x55102162a73da7be0e3cdd7225ccc51872b77dbe56e60f45b905937a8315f3a4", + "block": 158426176, + "gasUsed": "1918175", + "effectiveGasPrice": "1000015420", + "l1FeeWei": "37917216941" + } + }, + "totals": { + "gasUsed": "6475274", + "liveGasPriceWei": "1015717", + "l1FeeWei": "135841461634", + "l1FeeMethod": "op-stack oracle getL1Fee", + "costWei": "6712887343092", + "cost": "0.000006712887343092", + "native": "ETH" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/plasma.json b/deployments/fork-v2/plasma.json new file mode 100644 index 0000000..4da47f7 --- /dev/null +++ b/deployments/fork-v2/plasma.json @@ -0,0 +1,162 @@ +{ + "chain": "plasma", + "chainId": 9745, + "mode": "anvil-fork", + "forkedFrom": "https://rpc.plasma.to", + "forkBlock": 35509945, + "at": "2026-10-19T23:08:47.203Z", + "seconds": 12, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0x2b037cefd10bf9f82b595a21852a9f6e18016338b46d936d6c0ab93b369980b9", + "block": 35509931, + "gasUsed": "1162822", + "effectiveGasPrice": "1000000008", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0xc5e46698aaf96a374b0ef3dfa153fbc11c007453f31dc814f09c0269760dc169", + "block": 35509932, + "gasUsed": "904910", + "effectiveGasPrice": "1000000008", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "60000000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000340aad21b3b7000000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0x4972e3828848e4005dd3030990bac7254d3ccb7109570fc843261c4ebcbd195e", + "block": 35509933, + "gasUsed": "1711545", + "effectiveGasPrice": "1000000008", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x3b0e436b86e70f804d8cfbe909197b57c19232be167761d9685ad3c4b48a7115", + "block": 35509934, + "gasUsed": "369083", + "effectiveGasPrice": "1000000008", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x86f0609d2da89e6da90ef40e45695e8da62715ee095f7d13b23d012678c4015f", + "block": 35509935, + "gasUsed": "408763", + "effectiveGasPrice": "1000000008", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "60000000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000340aad21b3b70000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x1152910a273813e4e6eefa38675eb9ceeac0fd56731c3065d40c7a407d4f0863", + "block": 35509936, + "gasUsed": "1918199", + "effectiveGasPrice": "1000000008", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475322", + "liveGasPriceWei": "9903498", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "64128338476356", + "cost": "0.000064128338476356", + "native": "XPL" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": null, + "ReputationRegistry": null, + "ValidationRegistry": null + } +} diff --git a/deployments/fork-v2/polygon.json b/deployments/fork-v2/polygon.json new file mode 100644 index 0000000..f0cc220 --- /dev/null +++ b/deployments/fork-v2/polygon.json @@ -0,0 +1,171 @@ +{ + "chain": "polygon", + "chainId": 137, + "mode": "anvil-fork", + "forkedFrom": "https://polygon-bor-rpc.publicnode.com", + "forkBlock": 95830507, + "at": "2026-10-19T23:05:13.875Z", + "seconds": 6, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0x6bda15550c12ab4e08b28e8ef757eb586e025d90fe7a165e7b8170aa890ac18b", + "block": 95830503, + "gasUsed": "1162822", + "effectiveGasPrice": "230087183569", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x0c31637ec9ecc1fbc437590c8d0241b69dd9857f72732e6754df95193f428a28", + "block": 95830504, + "gasUsed": "904910", + "effectiveGasPrice": "204823910450", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "50000000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e6160000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000002b5e3af16b18800000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0xd877adcf5aa3f0f0d8074057e6fdaf3e2022d51ff6a604d9e8898a85a8c82d18", + "block": 95830505, + "gasUsed": "1711545", + "effectiveGasPrice": "182600178605", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x14647edf0b65246b88a01e5b406bc7fa3c0e2035595b5909df38498e5658d050", + "block": 95830506, + "gasUsed": "369083", + "effectiveGasPrice": "163321624714", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x5c06bfb19d4a2eca0e2b1d7cf2b7ee888cfffebbde4657cb664d7673f3980b41", + "block": 95830507, + "gasUsed": "408763", + "effectiveGasPrice": "146111190626", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "50000000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a052268370000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000002b5e3af16b188000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x44a2f03cf2620635c94dde7be78827d3f72ff5285909cc4351ae81992cc4b469", + "block": 95830508, + "gasUsed": "1918199", + "effectiveGasPrice": "131049644570", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475322", + "liveGasPriceWei": "247754752360", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "1604291798561259920", + "cost": "1.60429179856125992", + "native": "POL" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/robinhood.json b/deployments/fork-v2/robinhood.json new file mode 100644 index 0000000..08b8d54 --- /dev/null +++ b/deployments/fork-v2/robinhood.json @@ -0,0 +1,171 @@ +{ + "chain": "robinhood", + "chainId": 4663, + "mode": "anvil-fork", + "forkedFrom": "https://rpc.mainnet.chain.robinhood.com/rpc", + "forkBlock": 92990470, + "at": "2026-10-19T23:08:12.544Z", + "seconds": 9, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0xb7b2dd26dfa5c440614cb3516b788e56329455ce64515ef08a880170af40495f", + "block": 92990369, + "gasUsed": "1162822", + "effectiveGasPrice": "1017514001", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0xe1c68530b21225cfa805b2a0f01593982a52ee57a68919e408a3c7936887d594", + "block": 92990370, + "gasUsed": "904910", + "effectiveGasPrice": "1015324751", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "2000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0xce42dde4b097dbf4ef42066421a3a6d08a5080f050d6ed0ef9e6b97f95ae304a", + "block": 92990371, + "gasUsed": "1711521", + "effectiveGasPrice": "1013409158", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xa5cd1b4f69f0ad1ff6dd6ff0e3e3b591bb284a577f96b4e7795415956c3e2c5e", + "block": 92990372, + "gasUsed": "369083", + "effectiveGasPrice": "1011733014", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x033aa7020481fdd369a60c80b131d35554b5e92a4c465f4591f1fe3c36b813fe", + "block": 92990373, + "gasUsed": "408763", + "effectiveGasPrice": "1010266388", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "2000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x58e5d81d0b8f4e77e319b8373a0b918c2329670aae1be6bd48891a723918b951", + "block": 92990374, + "gasUsed": "1918175", + "effectiveGasPrice": "1008983090", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475274", + "liveGasPriceWei": "20016000", + "l1FeeWei": "0", + "l1FeeMethod": "arbitrum NodeInterface: 0 L1 gas units", + "costWei": "129609084384000", + "cost": "0.000129609084384", + "native": "ETH" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/rootstock.json b/deployments/fork-v2/rootstock.json new file mode 100644 index 0000000..8d719bc --- /dev/null +++ b/deployments/fork-v2/rootstock.json @@ -0,0 +1,162 @@ +{ + "chain": "rootstock", + "chainId": 30, + "mode": "anvil-fork", + "forkedFrom": "https://public-node.rsk.co", + "forkBlock": 9339619, + "at": "2026-10-19T23:08:27.539Z", + "seconds": 6, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0x00d017432794b934117b15ac688e623691e1a297b04d0240137457cbdd35cdfb", + "block": 9339620, + "gasUsed": "1162822", + "effectiveGasPrice": "2000000000", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0xacda5fb7549044433bc34ecc204cfbafeb86fdaee32cb2984728873064144c8f", + "block": 9339621, + "gasUsed": "904910", + "effectiveGasPrice": "1904070550", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "60000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000003691d6afc0000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0xd873619b761ca2d8bbe02544986aecee03295f8d5cf0215f8a47123413d7dbbc", + "block": 9339622, + "gasUsed": "1711521", + "effectiveGasPrice": "1811514294", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xad88606006f65383640f35607e8c1fe398fb8f750f80244adcd57c188198deb7", + "block": 9339623, + "gasUsed": "369083", + "effectiveGasPrice": "1744798102", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0xd0b73e18cfd74fc13659b6c16d84f11001332bba81bd25650fd2d3807d908253", + "block": 9339624, + "gasUsed": "408763", + "effectiveGasPrice": "1658570648", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "60000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000003691d6afc00000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0xe81c007c3123e975b790db58c3686c2293c6244cfe2af405fa87fe82e9dc7551", + "block": 9339625, + "gasUsed": "1918175", + "effectiveGasPrice": "1582979300", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475274", + "liveGasPriceWei": "26065600", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "168781901974400", + "cost": "0.0001687819019744", + "native": "RBTC" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": null, + "ReputationRegistry": null, + "ValidationRegistry": null + } +} diff --git a/deployments/fork-v2/scroll.json b/deployments/fork-v2/scroll.json new file mode 100644 index 0000000..054b940 --- /dev/null +++ b/deployments/fork-v2/scroll.json @@ -0,0 +1,171 @@ +{ + "chain": "scroll", + "chainId": 534352, + "mode": "anvil-fork", + "forkedFrom": "https://rpc.scroll.io", + "forkBlock": 35434108, + "at": "2026-10-19T23:06:42.094Z", + "seconds": 6, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0x97892f37919462ae8f3e44de7611b85d05c68303adcf7dce0a1f6f5ecbea1555", + "block": 35434108, + "gasUsed": "1162822", + "effectiveGasPrice": "1000105069", + "l1FeeWei": "15130847943833" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x05dc34a063577a5a29881189cb5c0f13bf58465d9e26c920520b3024794eed77", + "block": 35434109, + "gasUsed": "904910", + "effectiveGasPrice": "1000093463", + "l1FeeWei": "10726864161564" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "2000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0x6f5cfececa1ba4e91beebe9dd7b5cfe158be983727791c78f9d5abd371325028", + "block": 35434110, + "gasUsed": "1711521", + "effectiveGasPrice": "1000082838", + "l1FeeWei": "26317743379629" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x636db2652cacc7e6ef7202b056ecf7ad31c0d8a26e1b26f6d29c7a465da28115", + "block": 35434111, + "gasUsed": "369083", + "effectiveGasPrice": "1000074256", + "l1FeeWei": "3479054867203" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x6f8b050da0961b8be79a6b0d47c648db2a6eb1c1525ec34e16682f1853dfc12d", + "block": 35434112, + "gasUsed": "408763", + "effectiveGasPrice": "1000065317", + "l1FeeWei": "4117780274377" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "2000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x4c7b833342eda879949f9c036cdce994c0fd624aa38774abbf3bc68ad210bfb4", + "block": 35434113, + "gasUsed": "1918175", + "effectiveGasPrice": "1000057487", + "l1FeeWei": "30864573347652" + } + }, + "totals": { + "gasUsed": "6475274", + "liveGasPriceWei": "120108", + "l1FeeWei": "90636863974258", + "l1FeeMethod": "scroll oracle getL1Fee", + "costWei": "91414596183850", + "cost": "0.00009141459618385", + "native": "ETH" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + } +} diff --git a/deployments/fork-v2/sonic.json b/deployments/fork-v2/sonic.json new file mode 100644 index 0000000..d87c667 --- /dev/null +++ b/deployments/fork-v2/sonic.json @@ -0,0 +1,162 @@ +{ + "chain": "sonic", + "chainId": 146, + "mode": "anvil-fork", + "forkedFrom": "https://rpc.soniclabs.com", + "forkBlock": 81327451, + "at": "2026-10-19T23:07:08.417Z", + "seconds": 5, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0x14627b93d9717aa562a2daa3457f4835289476b2caaf6fef8ff1e8d61d74acf7", + "block": 81327443, + "gasUsed": "1162822", + "effectiveGasPrice": "44784296225", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x2c25234897ae197540aa6bc76d3b1f1220e3122e35c496210b57ab70c20701d8", + "block": 81327444, + "gasUsed": "904910", + "effectiveGasPrice": "39313804865", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "80000000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e6160000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000004563918244f4000000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0x69bb436d6a8e9614c443117d815ea8749126d137dd97a0fdb6036b063aba8eb0", + "block": 81327445, + "gasUsed": "1711545", + "effectiveGasPrice": "34526312785", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xfa8d0f87f68512cb15a1ce08466ad0791f6dac3ba53d2410b705bddcb1ea37e1", + "block": 81327446, + "gasUsed": "369083", + "effectiveGasPrice": "30338392777", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0xfa31df1661c7b3565caa968714f99c51644b15255fe68dd4ade19943ff38d62c", + "block": 81327447, + "gasUsed": "408763", + "effectiveGasPrice": "26671635095", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "80000000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a052268370000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000004563918244f40000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x87cf7bfd85c8be710290afc30483592fa66139d2526bba13f060d9c67874010c", + "block": 81327448, + "gasUsed": "1918199", + "effectiveGasPrice": "23463205389", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475322", + "liveGasPriceWei": "55000000000", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "356142710000000000", + "cost": "0.35614271", + "native": "S" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": null, + "ReputationRegistry": null, + "ValidationRegistry": null + } +} diff --git a/deployments/fork-v2/stable.json b/deployments/fork-v2/stable.json new file mode 100644 index 0000000..e0caf5e --- /dev/null +++ b/deployments/fork-v2/stable.json @@ -0,0 +1,162 @@ +{ + "chain": "stable", + "chainId": 988, + "mode": "anvil-fork", + "forkedFrom": "https://rpc.stable.xyz", + "forkBlock": 43687424, + "at": "2026-10-19T23:09:00.014Z", + "seconds": 8, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0xc4af0c6c848f66d1789cacc2958fef6a980679e59e9811ee834f94843fe5f687", + "block": 43687413, + "gasUsed": "1162822", + "effectiveGasPrice": "1875062500", + "l1FeeWei": "0" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x7204789ac9b1bc1c960403b2b2656411bee37eb8687e7d41bda508265ff9f337", + "block": 43687414, + "gasUsed": "904910", + "effectiveGasPrice": "1768223543", + "l1FeeWei": "0" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "5000000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e61600000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0x1c4c86f0ff938fa82e8a4e89aec3b3b056f5324bdb7f20324b3396132c242950", + "block": 43687415, + "gasUsed": "1711533", + "effectiveGasPrice": "1673933534", + "l1FeeWei": "0" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xdc2038fdb3f61a8535a3dcc5d33da009cb5e8d7fbb8a113c7c7061fdac07a7f2", + "block": 43687416, + "gasUsed": "369083", + "effectiveGasPrice": "1592575491", + "l1FeeWei": "0" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0xc02ae8e294fc2102eceb93d619b3713a352d281aa6eb35fd0d850ccaf742633f", + "block": 43687417, + "gasUsed": "408763", + "effectiveGasPrice": "1519050329", + "l1FeeWei": "0" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "5000000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a0522683700000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000004563918244f4000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x4a56e4373482f429043f05f3ce41737d8139f20e5b3a1bcc1d9bf4f5fe995df2", + "block": 43687418, + "gasUsed": "1918187", + "effectiveGasPrice": "1454699460", + "l1FeeWei": "0" + } + }, + "totals": { + "gasUsed": "6475298", + "liveGasPriceWei": "1125000000", + "l1FeeWei": "0", + "l1FeeMethod": "none (execution gas only)", + "costWei": "7284710250000000", + "cost": "0.00728471025", + "native": "USDT0" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": null, + "ReputationRegistry": null, + "ValidationRegistry": null + } +} diff --git a/deployments/fork-v2/unichain.json b/deployments/fork-v2/unichain.json new file mode 100644 index 0000000..852a5a2 --- /dev/null +++ b/deployments/fork-v2/unichain.json @@ -0,0 +1,162 @@ +{ + "chain": "unichain", + "chainId": 130, + "mode": "anvil-fork", + "forkedFrom": "https://mainnet.unichain.org", + "forkBlock": 61702856, + "at": "2026-10-19T23:06:57.321Z", + "seconds": 8, + "deployer": "0x33275DDdb0cB5Bc120740e048B030F107F144Ac8", + "startNonce": 0, + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xDC11e47D869e8FA2918A3e5fe09D76e8C11Fe675", + "nonce": 0, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc0000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "185b34b837ea7e0689bf599043cc79dba5b183dc0a660bb09879eb8da2cabd70", + "txHash": "0xe83ecdc851e68ed1921630608b7f1c14262fe858c108ece6a97a35c246d8cb3d", + "block": 61702845, + "gasUsed": "1162822", + "effectiveGasPrice": "1000500000", + "l1FeeWei": "14615147616" + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0xf076db3206a34c46241b3ce8758d19e23020cddb5d445c32bb5b7fe7ee022208", + "block": 61702846, + "gasUsed": "904910", + "effectiveGasPrice": "1000500000", + "l1FeeWei": "11202178770" + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x500A9cE7CADC357C8b477B24e7Bf7D7A7CD3df22", + "nonce": 2, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x93F70a097Ed45889f69759e5F62cDe0B9319E616", + "86400", + "2000000000000000", + "10000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc00000000000000000000000093f70a097ed45889f69759e5f62cde0b9319e616000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000002710", + "sourceSha256": "46bede16b7872b00005812a4f6899cfb8f151649a620f740122f81a1bd0fa22d", + "txHash": "0xc981d29d0f1bb9109cd68c99d1610b8cf42a891e3d63e8ea4bb5a35c327c02f2", + "block": 61702847, + "gasUsed": "1711521", + "effectiveGasPrice": "1000500000", + "l1FeeWei": "18862985546" + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0xef3450a304FD3B239ea90254370adD900b40C710", + "nonce": 3, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xfc0123da6fc278ad88a07a255ddd743a2d47f4aed1dd8b07f1c0f0e9098f9061", + "block": 61702848, + "gasUsed": "369083", + "effectiveGasPrice": "1000500000", + "l1FeeWei": "4764654725" + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0xd01f1026c372749873EA32BDB017434a05226837", + "nonce": 4, + "args": [ + "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553" + ], + "constructorArgs": "0x0000000000000000000000005c18b53c186d76fcb8c7e4ba4d6b6af79dd38553", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x2c9573d8ec42642098325c4e4417733d0b684b0fd4a120bf3404cc97ce216007", + "block": 61702849, + "gasUsed": "408763", + "effectiveGasPrice": "1000500000", + "l1FeeWei": "5640420430" + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x5C18B53C186d76fcB8c7E4Ba4D6b6aF79dD38553", + "nonce": 5, + "args": [ + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0xd01f1026c372749873EA32BDB017434a05226837", + "86400", + "2000000000000000", + "1000" + ], + "constructorArgs": "0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000003c44cdddb6a900fa2b585dd299e03d12fa4293bc000000000000000000000000d01f1026c372749873ea32bdb017434a05226837000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d000000000000000000000000000000000000000000000000000000000000000003e8", + "sourceSha256": "0d94db694bf2e39bfaca5398e2fcdef5b11942a656fab3de7f4468f40fd78746", + "txHash": "0x37d530d81192902cbb3deb12de9caac7e2a3452d3f63965593cda8b98805c021", + "block": 61702850, + "gasUsed": "1918175", + "effectiveGasPrice": "1000500000", + "l1FeeWei": "21269469942" + } + }, + "totals": { + "gasUsed": "6475274", + "liveGasPriceWei": "1500000", + "l1FeeWei": "76354857029", + "l1FeeMethod": "op-stack oracle getL1Fee", + "costWei": "9789265857029", + "cost": "0.000009789265857029", + "native": "ETH" + }, + "smoke": [ + "PredgeAgentValidatorV2 runtime == compiled", + "AgentJobV2 runtime == compiled", + "PredgeValidatorBondV2 runtime == compiled", + "PredgeSettlementV2 runtime == compiled", + "MockRefundProtocolV2 runtime == compiled", + "PredgeRefundArbiterV2 runtime == compiled", + "registry.owner == owner (not the deployer)", + "registry.pendingOwner == 0", + "bond.owner == owner (not the deployer)", + "bond.pendingOwner == 0", + "settlement.owner == owner (not the deployer)", + "settlement.pendingOwner == 0", + "arbiter.owner == owner (not the deployer)", + "arbiter.pendingOwner == 0", + "registry.operator == operator", + "bond.operator == operator", + "arbiter.operator == operator", + "no contract has a renounceOwnership", + "registry.identityRegistry", + "job.evaluationWindow", + "job.jobCount == 0", + "bond.job == AgentJobV2", + "bond params", + "bond.wouldSlash(unknown) == false", + "arbiter.refundProtocol == mock", + "mock.arbiter == arbiter", + "arbiter params", + "registry.requestHashFor is domain separated" + ], + "e2e": null, + "attacksBlocked": null, + "setVersion": "2026-10-20-v2", + "erc8004": { + "IdentityRegistry": null, + "ReputationRegistry": null, + "ValidationRegistry": null + } +} diff --git a/deployments/v2/arbitrum.json b/deployments/v2/arbitrum.json new file mode 100644 index 0000000..a744b12 --- /dev/null +++ b/deployments/v2/arbitrum.json @@ -0,0 +1,314 @@ +{ + "chain": "arbitrum", + "chainId": 42161, + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "owner": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "setVersion": "2026-10-23-v2.1", + "compiler": { + "solc": "v0.8.26+commit.8a97fa7a", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun" + } + }, + "deployedAt": "2026-10-26T15:30:49.059Z", + "explorer": "https://arbiscan.io", + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D", + "nonce": 6, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "188c8c4f4d75ca748cad006a4b30a825d2bc9e9725e42085685000e3ee0232dd", + "txHash": "0x9447819e5760f02a0a81a1a23af0ae8834c0fc2cc27086c5810c1b617d54fd6e", + "block": 518479783, + "gasUsed": "1186582", + "effectiveGasPrice": "20000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "nonce": 7, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "c83bd1c9ce4e8643b39246586619b7a12e203653b3f03386de03ab58b58c4a9a", + "txHash": "0xeebdbded558914e4be1817283d57a2304b2b30806b8d964a92630ad715f6fc6c", + "block": 518479795, + "gasUsed": "934768", + "effectiveGasPrice": "20000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x97E960Fe3D7759af8f51A95319859E60Df554329", + "nonce": 8, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "2000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000005c673c6abf3c60c3f6ce3f1509f5d99c08f8b13f00000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "0b84858a48a63548f0b889ca451587c6e57c81cf3b239e6521188f58aa8c694d", + "txHash": "0x76e40bea03d53f037bb6789b3c0806895214941196a936ab6ea8cb77c9118648", + "block": 518479805, + "gasUsed": "1474589", + "effectiveGasPrice": "20052000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24", + "nonce": 9, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d1821144", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x8f34c100da04c7e8840695c5911f08695df16be21d4062402fdabb5ea0230988", + "block": 518479824, + "gasUsed": "375784", + "effectiveGasPrice": "20000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "nonce": 10, + "args": [ + "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + ], + "constructorArgs": "0x000000000000000000000000f404f908cb1ba47c01ab8526b725aaa940557ea9", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x80854ff2ec326cd76d3a6222386ada1b424c75c8ec2ab9b2b49da9c79abe9079", + "block": 518479838, + "gasUsed": "416467", + "effectiveGasPrice": "20114000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9", + "nonce": 11, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "86400", + "2000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e29200000000000000000000000050041fdab10f8f56e24fa9ca806f082ccaeb05e9000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "b5dc07973805879660216a2047ce1471f6a05abeb49c07466efc77e0c5f56bcf", + "txHash": "0x91db814108fc0b6c07d27e950ce3e7f56987a13562013c5dc217076ee6638da8", + "block": 518479849, + "gasUsed": "1957501", + "effectiveGasPrice": "20052000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + } + }, + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + }, + "smoke": [ + { + "check": "PredgeAgentValidatorV2 runtime == compiled", + "ok": true, + "detail": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D" + }, + { + "check": "AgentJobV2 runtime == compiled", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "PredgeValidatorBondV2 runtime == compiled", + "ok": true, + "detail": "0x97E960Fe3D7759af8f51A95319859E60Df554329" + }, + { + "check": "PredgeSettlementV2 runtime == compiled", + "ok": true, + "detail": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24" + }, + { + "check": "MockRefundProtocolV2 runtime == compiled", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "PredgeRefundArbiterV2 runtime == compiled", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "registry.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "registry.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "bond.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "settlement.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "settlement.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "arbiter.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "registry.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "bond.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "arbiter.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "no contract has a renounceOwnership", + "ok": true, + "detail": "" + }, + { + "check": "registry.identityRegistry", + "ok": true, + "detail": "0x0000000000000000000000000000000000000000" + }, + { + "check": "job.evaluationWindow", + "ok": true, + "detail": "" + }, + { + "check": "job.jobCount == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.job == AgentJobV2", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "bond params", + "ok": true, + "detail": "" + }, + { + "check": "bond.verdictFor(no job) == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond has no challenge() (v2.1: false verdicts are refused, not slashed)", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.refundProtocol == mock", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "mock.arbiter == arbiter", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "arbiter params", + "ok": true, + "detail": "" + }, + { + "check": "registry.requestHashFor is domain separated", + "ok": true, + "detail": "" + }, + { + "check": "bond.specHashFor is domain separated", + "ok": true, + "detail": "" + } + ], + "legacy": "predge-robinhood/deploy/arbitrum-one (deployer 0x1d48…cF3c): validator, job, bond v2.1, settlement; no arbiter" +} diff --git a/deployments/v2/arc.json b/deployments/v2/arc.json new file mode 100644 index 0000000..1fe2f01 --- /dev/null +++ b/deployments/v2/arc.json @@ -0,0 +1,326 @@ +{ + "chain": "arc", + "chainId": 5042, + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "owner": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "setVersion": "2026-10-23-v2.1", + "compiler": { + "solc": "v0.8.26+commit.8a97fa7a", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun" + } + }, + "deployedAt": "2026-10-23T21:39:59.380Z", + "explorer": "https://explorer.arc.io", + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D", + "nonce": 6, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "188c8c4f4d75ca748cad006a4b30a825d2bc9e9725e42085685000e3ee0232dd", + "txHash": "0x5e255a03b2dfc5b9067ff243645740401269003f56e3b34dbc84afb52695cd99", + "block": 27518801, + "gasUsed": "1167799", + "effectiveGasPrice": "22281312500", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "nonce": 7, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "c83bd1c9ce4e8643b39246586619b7a12e203653b3f03386de03ab58b58c4a9a", + "txHash": "0x6a8877fc737f422363f7bbcf51a086b0401fe009e2d7cb5f092f040f672b4b9a", + "block": 27518804, + "gasUsed": "920066", + "effectiveGasPrice": "22281312500", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x97E960Fe3D7759af8f51A95319859E60Df554329", + "nonce": 8, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "5000000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000005c673c6abf3c60c3f6ce3f1509f5d99c08f8b13f0000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "0b84858a48a63548f0b889ca451587c6e57c81cf3b239e6521188f58aa8c694d", + "txHash": "0x89623fb5dabfb8bd9b804d04c32d275cc6864aaaed592c081b8a5a0412a07cb9", + "block": 27518808, + "gasUsed": "1453368", + "effectiveGasPrice": "22281312500", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24", + "nonce": 9, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d1821144", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x0f8e45d1156d6480396c23c3d707fcd9be54896b7699be032ac6e44f8fc4db26", + "block": 27518810, + "gasUsed": "369059", + "effectiveGasPrice": "22623509375", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "nonce": 10, + "args": [ + "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + ], + "constructorArgs": "0x000000000000000000000000f404f908cb1ba47c01ab8526b725aaa940557ea9", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x6dbd184dbc3ad8692ba13928e6f411dfadf752d9925bde25cdc931ea67885c3e", + "block": 27518813, + "gasUsed": "408763", + "effectiveGasPrice": "22623509375", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9", + "nonce": 11, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "86400", + "5000000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e29200000000000000000000000050041fdab10f8f56e24fa9ca806f082ccaeb05e900000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "b5dc07973805879660216a2047ce1471f6a05abeb49c07466efc77e0c5f56bcf", + "txHash": "0xe7889a21d05c3967c2193f1e54cb43d722607e7eeba77ec873a9b1833e486c8f", + "block": 27518817, + "gasUsed": "1930219", + "effectiveGasPrice": "22623509375", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + } + }, + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + }, + "smoke": [ + { + "check": "PredgeAgentValidatorV2 runtime == compiled", + "ok": true, + "detail": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D" + }, + { + "check": "AgentJobV2 runtime == compiled", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "PredgeValidatorBondV2 runtime == compiled", + "ok": true, + "detail": "0x97E960Fe3D7759af8f51A95319859E60Df554329" + }, + { + "check": "PredgeSettlementV2 runtime == compiled", + "ok": true, + "detail": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24" + }, + { + "check": "MockRefundProtocolV2 runtime == compiled", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "PredgeRefundArbiterV2 runtime == compiled", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "registry.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "registry.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "bond.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "settlement.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "settlement.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "arbiter.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "registry.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "bond.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "arbiter.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "no contract has a renounceOwnership", + "ok": true, + "detail": "" + }, + { + "check": "registry.identityRegistry", + "ok": true, + "detail": "0x0000000000000000000000000000000000000000" + }, + { + "check": "job.evaluationWindow", + "ok": true, + "detail": "" + }, + { + "check": "job.jobCount == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.job == AgentJobV2", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "bond params", + "ok": true, + "detail": "" + }, + { + "check": "bond.verdictFor(no job) == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond has no challenge() (v2.1: false verdicts are refused, not slashed)", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.refundProtocol == mock", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "mock.arbiter == arbiter", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "arbiter params", + "ok": true, + "detail": "" + }, + { + "check": "registry.requestHashFor is domain separated", + "ok": true, + "detail": "" + }, + { + "check": "bond.specHashFor is domain separated", + "ok": true, + "detail": "" + } + ], + "legacy": "predge-arc/deployments/arc-mainnet (deployer 0xEc7A…6223): full set incl. bond v2.1 + arbiter v2", + "supersedes": { + "setVersion": "2026-10-20-v2", + "record": "arc.v2-0.json", + "addresses": { + "PredgeAgentValidatorV2": "0xACC29BF9Ae35e51d36792C3B20D0Ac65f86Dd495", + "AgentJobV2": "0x550b360840C6614aD70E91CC6673FF3AB2dFd98C", + "PredgeValidatorBondV2": "0xa4BB68E55398c4EDD958DD6B32460D2018F78881", + "PredgeSettlementV2": "0x60991fE6a7afDcC79bDc43b754acE5c16158edb3", + "MockRefundProtocolV2": "0x2768dBC17728403BAE6D8838e4AeC63bE21a9dA7", + "PredgeRefundArbiterV2": "0x72FAEb8eE70816Dbf66722657cc4Bea314EB5FE3" + } + } +} diff --git a/deployments/v2/arc.v2-0.json b/deployments/v2/arc.v2-0.json new file mode 100644 index 0000000..c00f0dd --- /dev/null +++ b/deployments/v2/arc.v2-0.json @@ -0,0 +1,310 @@ +{ + "chain": "arc", + "chainId": 5042, + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "owner": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "setVersion": "2026-10-20-v2", + "compiler": { + "solc": "v0.8.26+commit.8a97fa7a", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun" + } + }, + "deployedAt": "2026-10-20T21:42:10.287Z", + "explorer": "https://explorer.arc.io", + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xACC29BF9Ae35e51d36792C3B20D0Ac65f86Dd495", + "nonce": 0, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "39ce8493d21512143f7c0fa37419a3cd14e1808bceb3af3e9680dfedb107ecd0", + "txHash": "0x85281d12e1c9a3db70261f2b2fb8d5b82ec5a88dca4389359934d22df52e790a", + "block": 27008331, + "gasUsed": "1162822", + "effectiveGasPrice": "20001749006", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x550b360840C6614aD70E91CC6673FF3AB2dFd98C", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "txHash": "0x860eb93b9a1652275f02f4da2cd07aaf34abe2e4a67640b8742ac7355f5813ef", + "block": 27008334, + "gasUsed": "904910", + "effectiveGasPrice": "20001749006", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0xa4BB68E55398c4EDD958DD6B32460D2018F78881", + "nonce": 2, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x550b360840C6614aD70E91CC6673FF3AB2dFd98C", + "86400", + "5000000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e292000000000000000000000000550b360840c6614ad70e91cc6673ff3ab2dfd98c00000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "887d0ae4945ea287ebddc2d9169a8f88074a75f871a49d0f7667b12f6b8f7e7d", + "txHash": "0x1c56cd30a31eeab6b9b344b9c9aa47c493eb0d25dcf17ec91a48f97b99327b69", + "block": 27008337, + "gasUsed": "1705859", + "effectiveGasPrice": "20001749006", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0x60991fE6a7afDcC79bDc43b754acE5c16158edb3", + "nonce": 3, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d1821144", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x7f3da7502d81decfb37c3e7498aa8bc4fc42acaca95e6e2f060e19d6b42f8640", + "block": 27008341, + "gasUsed": "369071", + "effectiveGasPrice": "20001749006", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x2768dBC17728403BAE6D8838e4AeC63bE21a9dA7", + "nonce": 4, + "args": [ + "0x72FAEb8eE70816Dbf66722657cc4Bea314EB5FE3" + ], + "constructorArgs": "0x00000000000000000000000072faeb8ee70816dbf66722657cc4bea314eb5fe3", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0xe28fe22cac1322b91746dd7341b21eb5394c096e309cb52853010d3e92d4afbb", + "block": 27008343, + "gasUsed": "408763", + "effectiveGasPrice": "20001749006", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x72FAEb8eE70816Dbf66722657cc4Bea314EB5FE3", + "nonce": 5, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x2768dBC17728403BAE6D8838e4AeC63bE21a9dA7", + "86400", + "5000000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000002768dbc17728403bae6d8838e4aec63be21a9da700000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000004563918244f400000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "4942e21f42a00e7034c5af74884dea2e0e1d4b7605d770d0b2ca9411df6e2a5e", + "txHash": "0x111d5cb1b0045dee19d114d44de12565c5336104735d62bb55f28a8bef12ab14", + "block": 27008347, + "gasUsed": "1898263", + "effectiveGasPrice": "20001749006", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + } + }, + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + }, + "smoke": [ + { + "check": "PredgeAgentValidatorV2 runtime == compiled", + "ok": true, + "detail": "0xACC29BF9Ae35e51d36792C3B20D0Ac65f86Dd495" + }, + { + "check": "AgentJobV2 runtime == compiled", + "ok": true, + "detail": "0x550b360840C6614aD70E91CC6673FF3AB2dFd98C" + }, + { + "check": "PredgeValidatorBondV2 runtime == compiled", + "ok": true, + "detail": "0xa4BB68E55398c4EDD958DD6B32460D2018F78881" + }, + { + "check": "PredgeSettlementV2 runtime == compiled", + "ok": true, + "detail": "0x60991fE6a7afDcC79bDc43b754acE5c16158edb3" + }, + { + "check": "MockRefundProtocolV2 runtime == compiled", + "ok": true, + "detail": "0x2768dBC17728403BAE6D8838e4AeC63bE21a9dA7" + }, + { + "check": "PredgeRefundArbiterV2 runtime == compiled", + "ok": true, + "detail": "0x72FAEb8eE70816Dbf66722657cc4Bea314EB5FE3" + }, + { + "check": "registry.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "registry.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "bond.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "settlement.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "settlement.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "arbiter.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "registry.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "bond.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "arbiter.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "no contract has a renounceOwnership", + "ok": true, + "detail": "" + }, + { + "check": "registry.identityRegistry", + "ok": true, + "detail": "0x0000000000000000000000000000000000000000" + }, + { + "check": "job.evaluationWindow", + "ok": true, + "detail": "" + }, + { + "check": "job.jobCount == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.job == AgentJobV2", + "ok": true, + "detail": "0x550b360840C6614aD70E91CC6673FF3AB2dFd98C" + }, + { + "check": "bond params", + "ok": true, + "detail": "" + }, + { + "check": "bond.wouldSlash(unknown) == false", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.refundProtocol == mock", + "ok": true, + "detail": "0x2768dBC17728403BAE6D8838e4AeC63bE21a9dA7" + }, + { + "check": "mock.arbiter == arbiter", + "ok": true, + "detail": "0x72FAEb8eE70816Dbf66722657cc4Bea314EB5FE3" + }, + { + "check": "arbiter params", + "ok": true, + "detail": "" + }, + { + "check": "registry.requestHashFor is domain separated", + "ok": true, + "detail": "" + } + ], + "legacy": "predge-arc/deployments/arc-mainnet (deployer 0xEc7A…6223): full set incl. bond v2.1 + arbiter v2", + "deprecated": { + "by": "2026-10-23-v2.1", + "record": "arc.json", + "reason": "Olympix BugPoCer scan of c47ed42 (23.10.2026): confirmed findings, fixed in v2.1. Not upgradeable, so the v2.0 instances stay on chain; do not use them." + } +} diff --git a/deployments/v2/avalanche.json b/deployments/v2/avalanche.json new file mode 100644 index 0000000..8389a02 --- /dev/null +++ b/deployments/v2/avalanche.json @@ -0,0 +1,314 @@ +{ + "chain": "avalanche", + "chainId": 43114, + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "owner": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "setVersion": "2026-10-23-v2.1", + "compiler": { + "solc": "v0.8.26+commit.8a97fa7a", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun" + } + }, + "deployedAt": "2026-10-26T15:35:48.943Z", + "explorer": "https://snowtrace.io", + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D", + "nonce": 6, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "188c8c4f4d75ca748cad006a4b30a825d2bc9e9725e42085685000e3ee0232dd", + "txHash": "0x1e5a5df3475b9d6f967faaf55d4e8ef0b9e97552ca381e606f013eddec05f386", + "block": 98616150, + "gasUsed": "1167799", + "effectiveGasPrice": "2500000150", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "nonce": 7, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "c83bd1c9ce4e8643b39246586619b7a12e203653b3f03386de03ab58b58c4a9a", + "txHash": "0x39a288bc83fab235632c8f0581d309980d903259d1809c5ac2d465fb721a6839", + "block": 98616151, + "gasUsed": "920066", + "effectiveGasPrice": "2500000150", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x97E960Fe3D7759af8f51A95319859E60Df554329", + "nonce": 8, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "500000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000005c673c6abf3c60c3f6ce3f1509f5d99c08f8b13f00000000000000000000000000000000000000000000000006f05b59d3b200000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "0b84858a48a63548f0b889ca451587c6e57c81cf3b239e6521188f58aa8c694d", + "txHash": "0xa6005e43e8e23435f3c742223e70258e1c96a427496c193fdb34a72382fa1cf5", + "block": 98616155, + "gasUsed": "1453368", + "effectiveGasPrice": "2500000150", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24", + "nonce": 9, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d1821144", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x9d9ef90f2820374c13b60fd7f8eee9001eb386cfb0aba7f2d8105e8155c61b80", + "block": 98616158, + "gasUsed": "369059", + "effectiveGasPrice": "2500000150", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "nonce": 10, + "args": [ + "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + ], + "constructorArgs": "0x000000000000000000000000f404f908cb1ba47c01ab8526b725aaa940557ea9", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x42e2420caa7c3d2251b7befc41b333734cec161708ec9b0cd68a4cecee3d9b74", + "block": 98616160, + "gasUsed": "408763", + "effectiveGasPrice": "2500000150", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9", + "nonce": 11, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "86400", + "500000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e29200000000000000000000000050041fdab10f8f56e24fa9ca806f082ccaeb05e9000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000006f05b59d3b200000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "b5dc07973805879660216a2047ce1471f6a05abeb49c07466efc77e0c5f56bcf", + "txHash": "0xdd082023b9577a6da2616738a25092bd341c211aca60dcfbb5a63a659e7c4f28", + "block": 98616163, + "gasUsed": "1930219", + "effectiveGasPrice": "2500000150", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + } + }, + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + }, + "smoke": [ + { + "check": "PredgeAgentValidatorV2 runtime == compiled", + "ok": true, + "detail": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D" + }, + { + "check": "AgentJobV2 runtime == compiled", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "PredgeValidatorBondV2 runtime == compiled", + "ok": true, + "detail": "0x97E960Fe3D7759af8f51A95319859E60Df554329" + }, + { + "check": "PredgeSettlementV2 runtime == compiled", + "ok": true, + "detail": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24" + }, + { + "check": "MockRefundProtocolV2 runtime == compiled", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "PredgeRefundArbiterV2 runtime == compiled", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "registry.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "registry.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "bond.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "settlement.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "settlement.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "arbiter.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "registry.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "bond.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "arbiter.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "no contract has a renounceOwnership", + "ok": true, + "detail": "" + }, + { + "check": "registry.identityRegistry", + "ok": true, + "detail": "0x0000000000000000000000000000000000000000" + }, + { + "check": "job.evaluationWindow", + "ok": true, + "detail": "" + }, + { + "check": "job.jobCount == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.job == AgentJobV2", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "bond params", + "ok": true, + "detail": "" + }, + { + "check": "bond.verdictFor(no job) == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond has no challenge() (v2.1: false verdicts are refused, not slashed)", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.refundProtocol == mock", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "mock.arbiter == arbiter", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "arbiter params", + "ok": true, + "detail": "" + }, + { + "check": "registry.requestHashFor is domain separated", + "ok": true, + "detail": "" + }, + { + "check": "bond.specHashFor is domain separated", + "ok": true, + "detail": "" + } + ], + "legacy": null +} diff --git a/deployments/v2/base.json b/deployments/v2/base.json new file mode 100644 index 0000000..3b9f58b --- /dev/null +++ b/deployments/v2/base.json @@ -0,0 +1,308 @@ +{ + "chain": "base", + "chainId": 8453, + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "owner": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "setVersion": "2026-10-23-v2.1", + "compiler": { + "solc": "v0.8.26+commit.8a97fa7a", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun" + } + }, + "deployedAt": "2026-10-23T21:44:53.724Z", + "explorer": "https://basescan.org", + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D", + "nonce": 6, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "188c8c4f4d75ca748cad006a4b30a825d2bc9e9725e42085685000e3ee0232dd", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "nonce": 7, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "c83bd1c9ce4e8643b39246586619b7a12e203653b3f03386de03ab58b58c4a9a", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x97E960Fe3D7759af8f51A95319859E60Df554329", + "nonce": 8, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "2000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000005c673c6abf3c60c3f6ce3f1509f5d99c08f8b13f00000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "0b84858a48a63548f0b889ca451587c6e57c81cf3b239e6521188f58aa8c694d", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24", + "nonce": 9, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d1821144", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "nonce": 10, + "args": [ + "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + ], + "constructorArgs": "0x000000000000000000000000f404f908cb1ba47c01ab8526b725aaa940557ea9", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9", + "nonce": 11, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "86400", + "2000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e29200000000000000000000000050041fdab10f8f56e24fa9ca806f082ccaeb05e9000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "b5dc07973805879660216a2047ce1471f6a05abeb49c07466efc77e0c5f56bcf", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + } + }, + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + }, + "smoke": [ + { + "check": "PredgeAgentValidatorV2 runtime == compiled", + "ok": true, + "detail": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D" + }, + { + "check": "AgentJobV2 runtime == compiled", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "PredgeValidatorBondV2 runtime == compiled", + "ok": true, + "detail": "0x97E960Fe3D7759af8f51A95319859E60Df554329" + }, + { + "check": "PredgeSettlementV2 runtime == compiled", + "ok": true, + "detail": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24" + }, + { + "check": "MockRefundProtocolV2 runtime == compiled", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "PredgeRefundArbiterV2 runtime == compiled", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "registry.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "registry.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "bond.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "settlement.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "settlement.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "arbiter.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "registry.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "bond.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "arbiter.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "no contract has a renounceOwnership", + "ok": true, + "detail": "" + }, + { + "check": "registry.identityRegistry", + "ok": true, + "detail": "0x0000000000000000000000000000000000000000" + }, + { + "check": "job.evaluationWindow", + "ok": true, + "detail": "" + }, + { + "check": "job.jobCount == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.job == AgentJobV2", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "bond params", + "ok": true, + "detail": "" + }, + { + "check": "bond.verdictFor(no job) == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond has no challenge() (v2.1: false verdicts are refused, not slashed)", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.refundProtocol == mock", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "mock.arbiter == arbiter", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "arbiter params", + "ok": true, + "detail": "" + }, + { + "check": "registry.requestHashFor is domain separated", + "ok": true, + "detail": "" + }, + { + "check": "bond.specHashFor is domain separated", + "ok": true, + "detail": "" + } + ], + "legacy": null, + "supersedes": { + "setVersion": "2026-10-20-v2", + "record": "base.v2-0.json", + "addresses": { + "PredgeAgentValidatorV2": "0xACC29BF9Ae35e51d36792C3B20D0Ac65f86Dd495", + "AgentJobV2": "0x550b360840C6614aD70E91CC6673FF3AB2dFd98C", + "PredgeValidatorBondV2": "0xa4BB68E55398c4EDD958DD6B32460D2018F78881", + "PredgeSettlementV2": "0x60991fE6a7afDcC79bDc43b754acE5c16158edb3", + "MockRefundProtocolV2": "0x2768dBC17728403BAE6D8838e4AeC63bE21a9dA7", + "PredgeRefundArbiterV2": "0x72FAEb8eE70816Dbf66722657cc4Bea314EB5FE3" + } + } +} diff --git a/deployments/v2/base.v2-0.json b/deployments/v2/base.v2-0.json new file mode 100644 index 0000000..7d4d4c5 --- /dev/null +++ b/deployments/v2/base.v2-0.json @@ -0,0 +1,292 @@ +{ + "chain": "base", + "chainId": 8453, + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "owner": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "setVersion": "2026-10-20-v2", + "compiler": { + "solc": "v0.8.26+commit.8a97fa7a", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun" + } + }, + "deployedAt": "2026-10-20T21:52:38.533Z", + "explorer": "https://basescan.org", + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xACC29BF9Ae35e51d36792C3B20D0Ac65f86Dd495", + "nonce": 0, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "39ce8493d21512143f7c0fa37419a3cd14e1808bceb3af3e9680dfedb107ecd0", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x550b360840C6614aD70E91CC6673FF3AB2dFd98C", + "nonce": 1, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "16050fa97d42c0982eac3bf4de36a4478c68165c7fbdfeac8e5b89960f6a8972", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0xa4BB68E55398c4EDD958DD6B32460D2018F78881", + "nonce": 2, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x550b360840C6614aD70E91CC6673FF3AB2dFd98C", + "86400", + "2000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e292000000000000000000000000550b360840c6614ad70e91cc6673ff3ab2dfd98c000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "887d0ae4945ea287ebddc2d9169a8f88074a75f871a49d0f7667b12f6b8f7e7d", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0x60991fE6a7afDcC79bDc43b754acE5c16158edb3", + "nonce": 3, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d1821144", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x2768dBC17728403BAE6D8838e4AeC63bE21a9dA7", + "nonce": 4, + "args": [ + "0x72FAEb8eE70816Dbf66722657cc4Bea314EB5FE3" + ], + "constructorArgs": "0x00000000000000000000000072faeb8ee70816dbf66722657cc4bea314eb5fe3", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0x72FAEb8eE70816Dbf66722657cc4Bea314EB5FE3", + "nonce": 5, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x2768dBC17728403BAE6D8838e4AeC63bE21a9dA7", + "86400", + "2000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000002768dbc17728403bae6d8838e4aec63be21a9da7000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "4942e21f42a00e7034c5af74884dea2e0e1d4b7605d770d0b2ca9411df6e2a5e", + "skipped": true, + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + } + }, + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + }, + "smoke": [ + { + "check": "PredgeAgentValidatorV2 runtime == compiled", + "ok": true, + "detail": "0xACC29BF9Ae35e51d36792C3B20D0Ac65f86Dd495" + }, + { + "check": "AgentJobV2 runtime == compiled", + "ok": true, + "detail": "0x550b360840C6614aD70E91CC6673FF3AB2dFd98C" + }, + { + "check": "PredgeValidatorBondV2 runtime == compiled", + "ok": true, + "detail": "0xa4BB68E55398c4EDD958DD6B32460D2018F78881" + }, + { + "check": "PredgeSettlementV2 runtime == compiled", + "ok": true, + "detail": "0x60991fE6a7afDcC79bDc43b754acE5c16158edb3" + }, + { + "check": "MockRefundProtocolV2 runtime == compiled", + "ok": true, + "detail": "0x2768dBC17728403BAE6D8838e4AeC63bE21a9dA7" + }, + { + "check": "PredgeRefundArbiterV2 runtime == compiled", + "ok": true, + "detail": "0x72FAEb8eE70816Dbf66722657cc4Bea314EB5FE3" + }, + { + "check": "registry.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "registry.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "bond.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "settlement.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "settlement.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "arbiter.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "registry.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "bond.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "arbiter.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "no contract has a renounceOwnership", + "ok": true, + "detail": "" + }, + { + "check": "registry.identityRegistry", + "ok": true, + "detail": "0x0000000000000000000000000000000000000000" + }, + { + "check": "job.evaluationWindow", + "ok": true, + "detail": "" + }, + { + "check": "job.jobCount == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.job == AgentJobV2", + "ok": true, + "detail": "0x550b360840C6614aD70E91CC6673FF3AB2dFd98C" + }, + { + "check": "bond params", + "ok": true, + "detail": "" + }, + { + "check": "bond.wouldSlash(unknown) == false", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.refundProtocol == mock", + "ok": true, + "detail": "0x2768dBC17728403BAE6D8838e4AeC63bE21a9dA7" + }, + { + "check": "mock.arbiter == arbiter", + "ok": true, + "detail": "0x72FAEb8eE70816Dbf66722657cc4Bea314EB5FE3" + }, + { + "check": "arbiter params", + "ok": true, + "detail": "" + }, + { + "check": "registry.requestHashFor is domain separated", + "ok": true, + "detail": "" + } + ], + "legacy": null, + "deprecated": { + "by": "2026-10-23-v2.1", + "record": "base.json", + "reason": "Olympix BugPoCer scan of c47ed42 (23.10.2026): confirmed findings, fixed in v2.1. Not upgradeable, so the v2.0 instances stay on chain; do not use them." + } +} diff --git a/deployments/v2/bnb.json b/deployments/v2/bnb.json new file mode 100644 index 0000000..323111e --- /dev/null +++ b/deployments/v2/bnb.json @@ -0,0 +1,314 @@ +{ + "chain": "bnb", + "chainId": 56, + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "owner": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "setVersion": "2026-10-23-v2.1", + "compiler": { + "solc": "v0.8.26+commit.8a97fa7a", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun" + } + }, + "deployedAt": "2026-10-26T15:33:38.050Z", + "explorer": "https://bscscan.com", + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D", + "nonce": 6, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "188c8c4f4d75ca748cad006a4b30a825d2bc9e9725e42085685000e3ee0232dd", + "txHash": "0x86c8e13f69b76f32fea933f6c0ae6c773a4011e2ba4d33fba4cfddb5b70b1f03", + "block": 129922016, + "gasUsed": "1167799", + "effectiveGasPrice": "50000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "nonce": 7, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "c83bd1c9ce4e8643b39246586619b7a12e203653b3f03386de03ab58b58c4a9a", + "txHash": "0x0dc0bc2ec9f8b2e2572df1695ff95a38726c794d357a1b7f44a72e3be413816b", + "block": 129922028, + "gasUsed": "920066", + "effectiveGasPrice": "50000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x97E960Fe3D7759af8f51A95319859E60Df554329", + "nonce": 8, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "6000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000005c673c6abf3c60c3f6ce3f1509f5d99c08f8b13f000000000000000000000000000000000000000000000000001550f7dca700000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "0b84858a48a63548f0b889ca451587c6e57c81cf3b239e6521188f58aa8c694d", + "txHash": "0x3936cf0264f114ba2321c2c8d7ffe737467150e1ad680bd09a6a4073b1a313a2", + "block": 129922036, + "gasUsed": "1453356", + "effectiveGasPrice": "50000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24", + "nonce": 9, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d1821144", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xdfb2be74b68ede481156d70f89ef351c73aa3a90b1b5a3bb447f6f7bfef5189b", + "block": 129922043, + "gasUsed": "369059", + "effectiveGasPrice": "50000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "nonce": 10, + "args": [ + "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + ], + "constructorArgs": "0x000000000000000000000000f404f908cb1ba47c01ab8526b725aaa940557ea9", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x47d7a6a687c92899e3e751078e06fe2ff8d27cadab315775997d9ebc0829c309", + "block": 129922051, + "gasUsed": "408763", + "effectiveGasPrice": "50000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9", + "nonce": 11, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "86400", + "6000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e29200000000000000000000000050041fdab10f8f56e24fa9ca806f082ccaeb05e90000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000001550f7dca700000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "b5dc07973805879660216a2047ce1471f6a05abeb49c07466efc77e0c5f56bcf", + "txHash": "0xc4d3c45794939de95393e4753245a1f1fce1bb50dc9786d673242608cb8f6917", + "block": 129922056, + "gasUsed": "1930207", + "effectiveGasPrice": "50000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + } + }, + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + }, + "smoke": [ + { + "check": "PredgeAgentValidatorV2 runtime == compiled", + "ok": true, + "detail": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D" + }, + { + "check": "AgentJobV2 runtime == compiled", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "PredgeValidatorBondV2 runtime == compiled", + "ok": true, + "detail": "0x97E960Fe3D7759af8f51A95319859E60Df554329" + }, + { + "check": "PredgeSettlementV2 runtime == compiled", + "ok": true, + "detail": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24" + }, + { + "check": "MockRefundProtocolV2 runtime == compiled", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "PredgeRefundArbiterV2 runtime == compiled", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "registry.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "registry.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "bond.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "settlement.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "settlement.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "arbiter.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "registry.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "bond.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "arbiter.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "no contract has a renounceOwnership", + "ok": true, + "detail": "" + }, + { + "check": "registry.identityRegistry", + "ok": true, + "detail": "0x0000000000000000000000000000000000000000" + }, + { + "check": "job.evaluationWindow", + "ok": true, + "detail": "" + }, + { + "check": "job.jobCount == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.job == AgentJobV2", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "bond params", + "ok": true, + "detail": "" + }, + { + "check": "bond.verdictFor(no job) == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond has no challenge() (v2.1: false verdicts are refused, not slashed)", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.refundProtocol == mock", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "mock.arbiter == arbiter", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "arbiter params", + "ok": true, + "detail": "" + }, + { + "check": "registry.requestHashFor is domain separated", + "ok": true, + "detail": "" + }, + { + "check": "bond.specHashFor is domain separated", + "ok": true, + "detail": "" + } + ], + "legacy": null +} diff --git a/deployments/v2/celo.json b/deployments/v2/celo.json new file mode 100644 index 0000000..a7eb801 --- /dev/null +++ b/deployments/v2/celo.json @@ -0,0 +1,314 @@ +{ + "chain": "celo", + "chainId": 42220, + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "owner": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "setVersion": "2026-10-23-v2.1", + "compiler": { + "solc": "v0.8.26+commit.8a97fa7a", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun" + } + }, + "deployedAt": "2026-10-26T15:40:12.601Z", + "explorer": "https://celoscan.io", + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D", + "nonce": 6, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "188c8c4f4d75ca748cad006a4b30a825d2bc9e9725e42085685000e3ee0232dd", + "txHash": "0xe0941f48d4398dcb54b85f8873c40851b01031a9b5e58bbe3b696a132d2643e7", + "block": 81128388, + "gasUsed": "1167799", + "effectiveGasPrice": "202500000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "nonce": 7, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "c83bd1c9ce4e8643b39246586619b7a12e203653b3f03386de03ab58b58c4a9a", + "txHash": "0xfce0827f1f2e9c5bde0f352368f4a26fe93633dd654f3f0ee3434ef860545ae9", + "block": 81128391, + "gasUsed": "920066", + "effectiveGasPrice": "202500000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x97E960Fe3D7759af8f51A95319859E60Df554329", + "nonce": 8, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "60000000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000005c673c6abf3c60c3f6ce3f1509f5d99c08f8b13f00000000000000000000000000000000000000000000000340aad21b3b7000000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "0b84858a48a63548f0b889ca451587c6e57c81cf3b239e6521188f58aa8c694d", + "txHash": "0xf046102f5c1b4e2ad245f43a7b062a7edbcaef2c991ca84ab19152e04f7af94d", + "block": 81128394, + "gasUsed": "1453380", + "effectiveGasPrice": "202500000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24", + "nonce": 9, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d1821144", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xe2a49e49034169c49641b1b7a51f8780005ace679f2b601d2896c02083eaed6b", + "block": 81128396, + "gasUsed": "369059", + "effectiveGasPrice": "202500000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "nonce": 10, + "args": [ + "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + ], + "constructorArgs": "0x000000000000000000000000f404f908cb1ba47c01ab8526b725aaa940557ea9", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x1d45f77c7e74a30c89735de4e50b4145c622b74935b8b0f95975f44cdbb958cb", + "block": 81128399, + "gasUsed": "408763", + "effectiveGasPrice": "202500000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9", + "nonce": 11, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "86400", + "60000000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e29200000000000000000000000050041fdab10f8f56e24fa9ca806f082ccaeb05e9000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000340aad21b3b7000000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "b5dc07973805879660216a2047ce1471f6a05abeb49c07466efc77e0c5f56bcf", + "txHash": "0x7c939dabe2282b212f2c4bce921206404f47d91b9255ee8a35518b420d6ec727", + "block": 81128403, + "gasUsed": "1930231", + "effectiveGasPrice": "202500000000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + } + }, + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + }, + "smoke": [ + { + "check": "PredgeAgentValidatorV2 runtime == compiled", + "ok": true, + "detail": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D" + }, + { + "check": "AgentJobV2 runtime == compiled", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "PredgeValidatorBondV2 runtime == compiled", + "ok": true, + "detail": "0x97E960Fe3D7759af8f51A95319859E60Df554329" + }, + { + "check": "PredgeSettlementV2 runtime == compiled", + "ok": true, + "detail": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24" + }, + { + "check": "MockRefundProtocolV2 runtime == compiled", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "PredgeRefundArbiterV2 runtime == compiled", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "registry.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "registry.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "bond.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "settlement.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "settlement.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "arbiter.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "registry.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "bond.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "arbiter.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "no contract has a renounceOwnership", + "ok": true, + "detail": "" + }, + { + "check": "registry.identityRegistry", + "ok": true, + "detail": "0x0000000000000000000000000000000000000000" + }, + { + "check": "job.evaluationWindow", + "ok": true, + "detail": "" + }, + { + "check": "job.jobCount == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.job == AgentJobV2", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "bond params", + "ok": true, + "detail": "" + }, + { + "check": "bond.verdictFor(no job) == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond has no challenge() (v2.1: false verdicts are refused, not slashed)", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.refundProtocol == mock", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "mock.arbiter == arbiter", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "arbiter params", + "ok": true, + "detail": "" + }, + { + "check": "registry.requestHashFor is domain separated", + "ok": true, + "detail": "" + }, + { + "check": "bond.specHashFor is domain separated", + "ok": true, + "detail": "" + } + ], + "legacy": null +} diff --git a/deployments/v2/optimism.json b/deployments/v2/optimism.json new file mode 100644 index 0000000..6eb86cd --- /dev/null +++ b/deployments/v2/optimism.json @@ -0,0 +1,314 @@ +{ + "chain": "optimism", + "chainId": 10, + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "owner": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "setVersion": "2026-10-23-v2.1", + "compiler": { + "solc": "v0.8.26+commit.8a97fa7a", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun" + } + }, + "deployedAt": "2026-10-26T15:24:39.044Z", + "explorer": "https://optimistic.etherscan.io", + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D", + "nonce": 6, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "188c8c4f4d75ca748cad006a4b30a825d2bc9e9725e42085685000e3ee0232dd", + "txHash": "0xadc721af493df128734f7ea5308c92276f3d9852b5924b245c13d5342d8c8ce0", + "block": 158714685, + "gasUsed": "1167799", + "effectiveGasPrice": "1000321", + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "nonce": 7, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "c83bd1c9ce4e8643b39246586619b7a12e203653b3f03386de03ab58b58c4a9a", + "txHash": "0x4f4ca06829919d21f64bd9513376e0b1d5bff18c67ac66352236fb705ec71bfd", + "block": 158714687, + "gasUsed": "920066", + "effectiveGasPrice": "1000321", + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x97E960Fe3D7759af8f51A95319859E60Df554329", + "nonce": 8, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "2000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000005c673c6abf3c60c3f6ce3f1509f5d99c08f8b13f00000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "0b84858a48a63548f0b889ca451587c6e57c81cf3b239e6521188f58aa8c694d", + "txHash": "0x81b7004b596297d772c961f87cfa64acf4a73a71ed73da2b25d555709b44e49f", + "block": 158714688, + "gasUsed": "1453356", + "effectiveGasPrice": "1000321", + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24", + "nonce": 9, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d1821144", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xaabca6392ed67fd71e9ee228f5d5d85107f0cdb42863213710c3476dd6c35ca8", + "block": 158714697, + "gasUsed": "369059", + "effectiveGasPrice": "1000320", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "nonce": 10, + "args": [ + "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + ], + "constructorArgs": "0x000000000000000000000000f404f908cb1ba47c01ab8526b725aaa940557ea9", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0xc42a09ea81b12ff4af92cb57100c273f3fcc8015f0d943ab991f74dc79718832", + "block": 158714699, + "gasUsed": "408763", + "effectiveGasPrice": "1000320", + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9", + "nonce": 11, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "86400", + "2000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e29200000000000000000000000050041fdab10f8f56e24fa9ca806f082ccaeb05e9000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "b5dc07973805879660216a2047ce1471f6a05abeb49c07466efc77e0c5f56bcf", + "txHash": "0x76ac395225af591eb661778e27f94b80ee5e32ae9548312fc1eba19ff4588e7b", + "block": 158714716, + "gasUsed": "1930207", + "effectiveGasPrice": "1000319", + "verified": { + "ok": true, + "via": "sourcify", + "note": "already verified" + } + } + }, + "erc8004": { + "IdentityRegistry": { + "address": "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "version": "2.0.0" + }, + "ReputationRegistry": { + "address": "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63", + "version": "2.0.0" + }, + "ValidationRegistry": { + "address": "0x8004Cc8439f36fd5F9F049D9fF86523Df6dAAB58", + "version": "2.0.0" + } + }, + "smoke": [ + { + "check": "PredgeAgentValidatorV2 runtime == compiled", + "ok": true, + "detail": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D" + }, + { + "check": "AgentJobV2 runtime == compiled", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "PredgeValidatorBondV2 runtime == compiled", + "ok": true, + "detail": "0x97E960Fe3D7759af8f51A95319859E60Df554329" + }, + { + "check": "PredgeSettlementV2 runtime == compiled", + "ok": true, + "detail": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24" + }, + { + "check": "MockRefundProtocolV2 runtime == compiled", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "PredgeRefundArbiterV2 runtime == compiled", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "registry.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "registry.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "bond.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "settlement.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "settlement.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "arbiter.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "registry.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "bond.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "arbiter.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "no contract has a renounceOwnership", + "ok": true, + "detail": "" + }, + { + "check": "registry.identityRegistry", + "ok": true, + "detail": "0x0000000000000000000000000000000000000000" + }, + { + "check": "job.evaluationWindow", + "ok": true, + "detail": "" + }, + { + "check": "job.jobCount == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.job == AgentJobV2", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "bond params", + "ok": true, + "detail": "" + }, + { + "check": "bond.verdictFor(no job) == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond has no challenge() (v2.1: false verdicts are refused, not slashed)", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.refundProtocol == mock", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "mock.arbiter == arbiter", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "arbiter params", + "ok": true, + "detail": "" + }, + { + "check": "registry.requestHashFor is domain separated", + "ok": true, + "detail": "" + }, + { + "check": "bond.specHashFor is domain separated", + "ok": true, + "detail": "" + } + ], + "legacy": null +} diff --git a/deployments/v2/sonic.json b/deployments/v2/sonic.json new file mode 100644 index 0000000..65099c8 --- /dev/null +++ b/deployments/v2/sonic.json @@ -0,0 +1,305 @@ +{ + "chain": "sonic", + "chainId": 146, + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "owner": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "setVersion": "2026-10-23-v2.1", + "compiler": { + "solc": "v0.8.26+commit.8a97fa7a", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun" + } + }, + "deployedAt": "2026-10-26T15:37:59.917Z", + "explorer": "https://sonicscan.org", + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D", + "nonce": 6, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "188c8c4f4d75ca748cad006a4b30a825d2bc9e9725e42085685000e3ee0232dd", + "txHash": "0x4461d9e17262f441e5770bbbe898cdda7db0d2b2c5406b8c12becb3361d3c75e", + "block": 81652981, + "gasUsed": "1196993", + "effectiveGasPrice": "50000000001", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "nonce": 7, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "c83bd1c9ce4e8643b39246586619b7a12e203653b3f03386de03ab58b58c4a9a", + "txHash": "0x9561e3368b5e418eec945a391af4cd246ce6cc9dc60e9dfc18542b90efe98cbf", + "block": 81652985, + "gasUsed": "943067", + "effectiveGasPrice": "50000000001", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x97E960Fe3D7759af8f51A95319859E60Df554329", + "nonce": 8, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "80000000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000005c673c6abf3c60c3f6ce3f1509f5d99c08f8b13f000000000000000000000000000000000000000000000004563918244f4000000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "0b84858a48a63548f0b889ca451587c6e57c81cf3b239e6521188f58aa8c694d", + "txHash": "0x234c00e7caf9f0ddb695184e567aea34d78b174139ede8aea06290f8c0831035", + "block": 81652987, + "gasUsed": "1489714", + "effectiveGasPrice": "50000000001", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24", + "nonce": 9, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d1821144", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0x0d3fa78a9429b1c4a337374c583edd74975940595b5457edfc9afe079764ee9c", + "block": 81652989, + "gasUsed": "378285", + "effectiveGasPrice": "50000000001", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "nonce": 10, + "args": [ + "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + ], + "constructorArgs": "0x000000000000000000000000f404f908cb1ba47c01ab8526b725aaa940557ea9", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x87efd6c02fff03ccb4be6b2f3320b6c4379818afe946868bda3e5e06b7a82664", + "block": 81652992, + "gasUsed": "418982", + "effectiveGasPrice": "50000000001", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9", + "nonce": 11, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "86400", + "80000000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e29200000000000000000000000050041fdab10f8f56e24fa9ca806f082ccaeb05e90000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000004563918244f4000000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "b5dc07973805879660216a2047ce1471f6a05abeb49c07466efc77e0c5f56bcf", + "txHash": "0xc8e66ba3402979ae1fe904fe144438d1702a24f96eef414cf045bc69208b6f8d", + "block": 81652993, + "gasUsed": "1978486", + "effectiveGasPrice": "50000000001", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + } + }, + "erc8004": { + "IdentityRegistry": null, + "ReputationRegistry": null, + "ValidationRegistry": null + }, + "smoke": [ + { + "check": "PredgeAgentValidatorV2 runtime == compiled", + "ok": true, + "detail": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D" + }, + { + "check": "AgentJobV2 runtime == compiled", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "PredgeValidatorBondV2 runtime == compiled", + "ok": true, + "detail": "0x97E960Fe3D7759af8f51A95319859E60Df554329" + }, + { + "check": "PredgeSettlementV2 runtime == compiled", + "ok": true, + "detail": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24" + }, + { + "check": "MockRefundProtocolV2 runtime == compiled", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "PredgeRefundArbiterV2 runtime == compiled", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "registry.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "registry.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "bond.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "settlement.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "settlement.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "arbiter.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "registry.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "bond.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "arbiter.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "no contract has a renounceOwnership", + "ok": true, + "detail": "" + }, + { + "check": "registry.identityRegistry", + "ok": true, + "detail": "0x0000000000000000000000000000000000000000" + }, + { + "check": "job.evaluationWindow", + "ok": true, + "detail": "" + }, + { + "check": "job.jobCount == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.job == AgentJobV2", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "bond params", + "ok": true, + "detail": "" + }, + { + "check": "bond.verdictFor(no job) == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond has no challenge() (v2.1: false verdicts are refused, not slashed)", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.refundProtocol == mock", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "mock.arbiter == arbiter", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "arbiter params", + "ok": true, + "detail": "" + }, + { + "check": "registry.requestHashFor is domain separated", + "ok": true, + "detail": "" + }, + { + "check": "bond.specHashFor is domain separated", + "ok": true, + "detail": "" + } + ], + "legacy": null +} diff --git a/deployments/v2/unichain.json b/deployments/v2/unichain.json new file mode 100644 index 0000000..e5f6736 --- /dev/null +++ b/deployments/v2/unichain.json @@ -0,0 +1,305 @@ +{ + "chain": "unichain", + "chainId": 130, + "deployer": "0xbB7A0c24677339045434273FFC2266B99E1e1301", + "operator": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "owner": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "setVersion": "2026-10-23-v2.1", + "compiler": { + "solc": "v0.8.26+commit.8a97fa7a", + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun" + } + }, + "deployedAt": "2026-10-26T15:28:24.836Z", + "explorer": "https://uniscan.xyz", + "contracts": { + "PredgeAgentValidatorV2": { + "name": "PredgeAgentValidatorV2", + "address": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D", + "nonce": 6, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x0000000000000000000000000000000000000000" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "188c8c4f4d75ca748cad006a4b30a825d2bc9e9725e42085685000e3ee0232dd", + "txHash": "0x38e3e6a28c46ae4339d9d929c688e0c4824bc7ebcaae017001c27b48145d1658", + "block": 62280091, + "gasUsed": "1167799", + "effectiveGasPrice": "1500000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "AgentJobV2": { + "name": "AgentJobV2", + "address": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "nonce": 7, + "args": [ + "259200" + ], + "constructorArgs": "0x000000000000000000000000000000000000000000000000000000000003f480", + "sourceSha256": "c83bd1c9ce4e8643b39246586619b7a12e203653b3f03386de03ab58b58c4a9a", + "txHash": "0x35345352f913199f14af4cc7caf9e6b5c98c2684879a9ee9113bea131dabc87d", + "block": 62280093, + "gasUsed": "920066", + "effectiveGasPrice": "1500000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeValidatorBondV2": { + "name": "PredgeValidatorBondV2", + "address": "0x97E960Fe3D7759af8f51A95319859E60Df554329", + "nonce": 8, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F", + "2000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e2920000000000000000000000005c673c6abf3c60c3f6ce3f1509f5d99c08f8b13f00000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "0b84858a48a63548f0b889ca451587c6e57c81cf3b239e6521188f58aa8c694d", + "txHash": "0xb223a1c0aea7a3afb1e5874fd387c8216ec5369c312b95ccd12e695e527dc42f", + "block": 62280096, + "gasUsed": "1453356", + "effectiveGasPrice": "1500000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeSettlementV2": { + "name": "PredgeSettlementV2", + "address": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24", + "nonce": 9, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d1821144", + "sourceSha256": "fb87e5ef6756eb261c854ccf1f711234a2e657718aecb2e2d116557aa4ab84b7", + "txHash": "0xffc3af35646f0dc4d6909e3e4c1f83de7200914d2190de8143b19b8a74cf1a90", + "block": 62280099, + "gasUsed": "369059", + "effectiveGasPrice": "1500000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "MockRefundProtocolV2": { + "name": "MockRefundProtocolV2", + "address": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "nonce": 10, + "args": [ + "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + ], + "constructorArgs": "0x000000000000000000000000f404f908cb1ba47c01ab8526b725aaa940557ea9", + "sourceSha256": "e4f170d51e3a526a89bd4c0d1090b1441334fe5a610a0b43fef0dca078d3e66d", + "txHash": "0x67509f87739a75dc0b5667845a9cda8dbc202f938a30148334082fb4993a5263", + "block": 62280101, + "gasUsed": "408763", + "effectiveGasPrice": "1500000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + }, + "PredgeRefundArbiterV2": { + "name": "PredgeRefundArbiterV2", + "address": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9", + "nonce": 11, + "args": [ + "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144", + "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292", + "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9", + "86400", + "2000000000000000", + "0" + ], + "constructorArgs": "0x0000000000000000000000009084f5000e07c7133d6da5ee4f271ab6d18211440000000000000000000000008fffb76c4329ddc8ee4a859ddf55ab48ba25e29200000000000000000000000050041fdab10f8f56e24fa9ca806f082ccaeb05e9000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000071afd498d00000000000000000000000000000000000000000000000000000000000000000000", + "sourceSha256": "b5dc07973805879660216a2047ce1471f6a05abeb49c07466efc77e0c5f56bcf", + "txHash": "0x29e263b3bf8a6595093e9a9847b7830c2c6154aee14246537073beeb5ef25055", + "block": 62280103, + "gasUsed": "1930207", + "effectiveGasPrice": "1500000", + "verified": { + "ok": true, + "via": "sourcify", + "match": "exact_match" + } + } + }, + "erc8004": { + "IdentityRegistry": null, + "ReputationRegistry": null, + "ValidationRegistry": null + }, + "smoke": [ + { + "check": "PredgeAgentValidatorV2 runtime == compiled", + "ok": true, + "detail": "0xE4CFdE5dbD9afD518b9c59136939607505bF643D" + }, + { + "check": "AgentJobV2 runtime == compiled", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "PredgeValidatorBondV2 runtime == compiled", + "ok": true, + "detail": "0x97E960Fe3D7759af8f51A95319859E60Df554329" + }, + { + "check": "PredgeSettlementV2 runtime == compiled", + "ok": true, + "detail": "0x6fac55c5187A9cdb519F77163C7e3fD9E87F6f24" + }, + { + "check": "MockRefundProtocolV2 runtime == compiled", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "PredgeRefundArbiterV2 runtime == compiled", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "registry.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "registry.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "bond.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "settlement.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "settlement.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.owner == owner (not the deployer)", + "ok": true, + "detail": "0x9084f5000E07C7133D6dA5eE4f271AB6D1821144" + }, + { + "check": "arbiter.pendingOwner == 0", + "ok": true, + "detail": "" + }, + { + "check": "registry.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "bond.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "arbiter.operator == operator", + "ok": true, + "detail": "0x8ffFB76c4329DDC8ee4a859DDF55AB48ba25e292" + }, + { + "check": "no contract has a renounceOwnership", + "ok": true, + "detail": "" + }, + { + "check": "registry.identityRegistry", + "ok": true, + "detail": "0x0000000000000000000000000000000000000000" + }, + { + "check": "job.evaluationWindow", + "ok": true, + "detail": "" + }, + { + "check": "job.jobCount == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond.job == AgentJobV2", + "ok": true, + "detail": "0x5C673c6aBF3c60C3F6Ce3f1509f5d99C08F8B13F" + }, + { + "check": "bond params", + "ok": true, + "detail": "" + }, + { + "check": "bond.verdictFor(no job) == 0", + "ok": true, + "detail": "" + }, + { + "check": "bond has no challenge() (v2.1: false verdicts are refused, not slashed)", + "ok": true, + "detail": "" + }, + { + "check": "arbiter.refundProtocol == mock", + "ok": true, + "detail": "0x50041FdAb10f8f56E24FA9CA806f082CcAeb05E9" + }, + { + "check": "mock.arbiter == arbiter", + "ok": true, + "detail": "0xf404F908cb1ba47C01AB8526B725aAA940557eA9" + }, + { + "check": "arbiter params", + "ok": true, + "detail": "" + }, + { + "check": "registry.requestHashFor is domain separated", + "ok": true, + "detail": "" + }, + { + "check": "bond.specHashFor is domain separated", + "ok": true, + "detail": "" + } + ], + "legacy": null +} diff --git a/deployments/v2/verify/AgentJobV2.json b/deployments/v2/verify/AgentJobV2.json new file mode 100644 index 0000000..3ebb803 --- /dev/null +++ b/deployments/v2/verify/AgentJobV2.json @@ -0,0 +1,32 @@ +{ + "language": "Solidity", + "sources": { + "v2/AgentJobV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {IAgentJobV2} from \"./interfaces/IAgentJobV2.sol\";\nimport {PullCredit} from \"./access/PullCredit.sol\";\n\n/// @title AgentJobV2\n/// @notice A minimal ERC-8183-style agent job with escrowed native value and an evaluator named at\n/// creation. The evaluator may be neither the client nor the provider (a trivial check: it\n/// cannot detect two addresses controlled by one party). In Predge's deployment the evaluator is the\n/// `PredgeValidatorBondV2` contract, which only accepts a verdict that agrees with the on-chain\n/// deliverable hash and moves the money in the same transaction.\n///\n/// Lifecycle:\n/// createJob (client escrows value, names provider, evaluator, spec and an expiry)\n/// submit (provider, strictly before `expiredAt`)\n/// complete / reject (evaluator, strictly before `submittedAt + evaluationWindow`)\n/// claimRefund (anyone may trigger; the CLIENT is credited) when\n/// - the job is still Open at or after `expiredAt`, or\n/// - the job is Submitted and the evaluator stayed silent until\n/// `submittedAt + evaluationWindow`.\n///\n/// Timeout policy (documented choice): if the evaluator never decides, the CLIENT is\n/// refunded, as ERC-8183 does on expiry. The alternative (pay the provider by default)\n/// would let a silent or bribed evaluator pay out unverified work. The cost of this\n/// choice is borne by the provider, who did the work; providers should only work for\n/// evaluators they trust to answer inside the window.\n///\n/// Money never leaves by push. Every payout, whether to provider or client, is credited\n/// to `credit[account]` and pulled with `withdraw` / `withdrawTo`, so a reverting or\n/// blocklisted recipient can neither force an outcome nor lock another party's funds.\n///\n/// @dev Native value only; amounts are in the chain's native wei. There is no owner, no\n/// admin, no upgrade path and no mutable parameter: `evaluationWindow` is immutable.\n/// Deliverable hashing: `sha256`. When the provider supplies the output bytes, the hash is\n/// computed on-chain with the sha256 precompile (so the stored hash provably belongs to\n/// those bytes); otherwise the provider's claimed hash is stored as given.\ncontract AgentJobV2 is IAgentJobV2, PullCredit {\n /// @notice Seconds the evaluator has to decide after a submission. Immutable, so it cannot\n /// be changed under a job that is already running.\n uint64 public immutable evaluationWindow;\n\n uint256 public jobCount;\n /// @notice Sum of every job's escrow that has not been credited yet (Open or Submitted).\n uint256 public totalEscrowed;\n\n mapping(uint256 => Job) private _jobs;\n\n event JobCreated(\n uint256 indexed jobId,\n address indexed client,\n address indexed evaluator,\n address provider,\n uint256 escrow,\n uint64 expiredAt,\n bytes32 specHash\n );\n event Submitted(uint256 indexed jobId, address indexed provider, bytes32 deliverable, bool hashedOnChain);\n event Completed(uint256 indexed jobId, address indexed evaluator, address indexed recipient, uint256 amount, bytes32 reason);\n event Rejected(uint256 indexed jobId, address indexed evaluator, address indexed recipient, uint256 amount, bytes32 reason);\n event Refunded(uint256 indexed jobId, address indexed caller, address indexed recipient, uint256 amount);\n\n error ZeroAddress();\n error ZeroEscrow();\n error BadExpiry();\n error BadWindow();\n error NotProvider();\n error NotEvaluator();\n error BadState();\n error PastDeadline();\n error NotExpired();\n error HashMismatch();\n error ZeroDeliverable();\n error EvaluatorNotIndependent();\n\n /// @param evaluationWindow_ seconds the evaluator has after a submission; must be non-zero.\n constructor(uint64 evaluationWindow_) {\n if (evaluationWindow_ == 0) revert BadWindow();\n evaluationWindow = evaluationWindow_;\n }\n\n /// @notice Client opens a job, escrows `msg.value`, and names the provider, the independent\n /// evaluator, the acceptance-test commitment and the expiry.\n /// @param provider the worker; paid on `complete`\n /// @param evaluator the only address that can complete or reject\n /// @param specHash the acceptance test commitment. For a job evaluated by `PredgeValidatorBondV2` it\n /// must be `bond.specHashFor(keccak256(abi.encode(expected, salt)))`: the client\n /// fixes the expected result here, hidden behind the salt.\n /// @param expiredAt unix time; the provider must submit strictly before it, after it the\n /// client can reclaim the escrow of a job that was never submitted\n function createJob(address provider, address evaluator, bytes32 specHash, uint64 expiredAt)\n external\n payable\n returns (uint256 jobId)\n {\n if (provider == address(0) || evaluator == address(0)) revert ZeroAddress();\n // Olympix 5.3.4: a client (or provider) that names itself evaluator would judge its own job.\n if (evaluator == msg.sender || evaluator == provider) revert EvaluatorNotIndependent();\n if (msg.value == 0) revert ZeroEscrow();\n if (expiredAt <= block.timestamp) revert BadExpiry();\n jobId = ++jobCount;\n totalEscrowed += msg.value;\n Job storage j = _jobs[jobId];\n j.client = msg.sender;\n j.provider = provider;\n j.evaluator = evaluator;\n j.escrow = msg.value;\n j.expiredAt = expiredAt;\n j.state = State.Open;\n j.specHash = specHash;\n emit JobCreated(jobId, msg.sender, evaluator, provider, msg.value, expiredAt, specHash);\n }\n\n /// @notice Provider submits the deliverable, strictly before `expiredAt`.\n /// @param deliverable sha256 of the output. Must be non-zero.\n /// @param output optional output bytes. If non-empty, `sha256(output)` must equal\n /// `deliverable` (reverts `HashMismatch` otherwise) and the job records that\n /// the hash was verified on-chain. If empty, the claimed hash is stored.\n function submit(uint256 jobId, bytes32 deliverable, bytes calldata output) external {\n Job storage j = _jobs[jobId];\n if (j.state != State.Open) revert BadState();\n if (msg.sender != j.provider) revert NotProvider();\n if (block.timestamp >= j.expiredAt) revert PastDeadline();\n if (deliverable == bytes32(0)) revert ZeroDeliverable();\n bool onChain = output.length != 0;\n if (onChain && sha256(output) != deliverable) revert HashMismatch();\n j.deliverable = deliverable;\n j.hashedOnChain = onChain;\n j.submittedAt = uint64(block.timestamp);\n j.state = State.Submitted;\n emit Submitted(jobId, msg.sender, deliverable, onChain);\n }\n\n /// @notice ERC-8183 `complete`. Evaluator only, while the job is Submitted and the\n /// evaluation window is still running. Credits the provider.\n /// @param reason the evaluator's attestation commitment (Predge: keccak256 of the signed verdict)\n function complete(uint256 jobId, bytes32 reason, bytes calldata) external {\n Job storage j = _decide(jobId, reason);\n j.state = State.Completed;\n uint256 amt = _release(j, j.provider);\n emit Completed(jobId, msg.sender, j.provider, amt, reason);\n }\n\n /// @notice ERC-8183 `reject`. Evaluator only, same timing as `complete`. Credits the client.\n function reject(uint256 jobId, bytes32 reason, bytes calldata) external {\n Job storage j = _decide(jobId, reason);\n j.state = State.Rejected;\n uint256 amt = _release(j, j.client);\n emit Rejected(jobId, msg.sender, j.client, amt, reason);\n }\n\n /// @notice Refund the client of a job that ran out of time. Callable by anyone (the credit\n /// always goes to the client): see the contract notes for the two cases.\n function claimRefund(uint256 jobId) external {\n Job storage j = _jobs[jobId];\n State s = j.state;\n if (s == State.Open) {\n if (block.timestamp < j.expiredAt) revert NotExpired();\n } else if (s == State.Submitted) {\n if (block.timestamp < uint256(j.submittedAt) + evaluationWindow) revert NotExpired();\n } else {\n revert BadState();\n }\n j.state = State.Expired;\n uint256 amt = _release(j, j.client);\n emit Refunded(jobId, msg.sender, j.client, amt);\n }\n\n /// @inheritdoc IAgentJobV2\n function getJob(uint256 jobId) external view returns (Job memory) {\n return _jobs[jobId];\n }\n\n function _decide(uint256 jobId, bytes32 reason) private returns (Job storage j) {\n j = _jobs[jobId];\n if (msg.sender != j.evaluator) revert NotEvaluator();\n if (j.state != State.Submitted) revert BadState();\n if (block.timestamp >= uint256(j.submittedAt) + evaluationWindow) revert PastDeadline();\n j.reason = reason;\n }\n\n function _release(Job storage j, address to) private returns (uint256 amt) {\n amt = j.escrow;\n j.escrow = 0;\n totalEscrowed -= amt;\n _addCredit(to, amt);\n }\n}\n" + }, + "v2/interfaces/IAgentJobV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @notice The slice of `AgentJobV2` that other Predge contracts read and drive.\ninterface IAgentJobV2 {\n /// @dev Forward-only, except that `Open` and `Submitted` may each end in `Expired`.\n enum State {\n None,\n Open,\n Submitted,\n Completed,\n Rejected,\n Expired\n }\n\n struct Job {\n address client; // pays; refunded on reject / expiry\n address provider; // does the work; paid on complete\n address evaluator; // the only address that can complete or reject\n uint256 escrow; // native value held for this job until it ends\n uint64 expiredAt; // the provider must submit strictly before this\n uint64 submittedAt; // 0 until submitted\n State state;\n bool hashedOnChain; // true if the deliverable hash was computed on-chain from submitted bytes\n bytes32 specHash; // the committed acceptance test (bond.specHashFor(commitment) for bonded jobs)\n bytes32 deliverable; // sha256 of the provider's output (zero until submitted)\n bytes32 reason; // the evaluator's attestation commitment (zero until decided)\n }\n\n function getJob(uint256 jobId) external view returns (Job memory);\n function complete(uint256 jobId, bytes32 reason, bytes calldata optParams) external;\n function reject(uint256 jobId, bytes32 reason, bytes calldata optParams) external;\n}\n" + }, + "v2/access/PullCredit.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title PullCredit\n/// @notice Pull-payment ledger shared by the Predge v2 contracts. Whoever is owed value is\n/// credited here and withdraws it themselves, so a recipient that reverts or is\n/// blocklisted can never force an outcome or lock anybody else's funds.\n/// @dev `totalCredit` is the sum of all outstanding credits; the inheriting contract's native\n/// balance is always at least `totalCredit` plus whatever it still escrows or bonds.\n/// Checks-effects-interactions: the credit is zeroed and the event emitted before the call.\nabstract contract PullCredit {\n /// @notice Withdrawable balance per account.\n mapping(address => uint256) public credit;\n /// @notice Sum of all credits not yet withdrawn.\n uint256 public totalCredit;\n\n event Withdrawn(address indexed account, address indexed to, uint256 amount);\n\n error NothingToWithdraw();\n error TransferFailed();\n error ZeroRecipient();\n\n /// @notice Withdraw the caller's whole credit to the caller.\n function withdraw() external {\n _withdraw(payable(msg.sender));\n }\n\n /// @notice Withdraw the caller's whole credit to `to`, e.g. when the caller's own address\n /// cannot receive value (blocklisted or a contract without a receive function).\n function withdrawTo(address payable to) external {\n if (to == address(0)) revert ZeroRecipient();\n _withdraw(to);\n }\n\n function _addCredit(address to, uint256 amount) internal {\n credit[to] += amount;\n totalCredit += amount;\n }\n\n function _withdraw(address payable to) private {\n uint256 amt = credit[msg.sender];\n if (amt == 0) revert NothingToWithdraw();\n credit[msg.sender] = 0;\n totalCredit -= amt;\n emit Withdrawn(msg.sender, to, amt);\n (bool ok,) = to.call{value: amt}(\"\");\n if (!ok) revert TransferFailed();\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/deployments/v2/verify/AgentJobV2.v2-0.json b/deployments/v2/verify/AgentJobV2.v2-0.json new file mode 100644 index 0000000..b330112 --- /dev/null +++ b/deployments/v2/verify/AgentJobV2.v2-0.json @@ -0,0 +1,32 @@ +{ + "language": "Solidity", + "sources": { + "v2/AgentJobV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {IAgentJobV2} from \"./interfaces/IAgentJobV2.sol\";\nimport {PullCredit} from \"./access/PullCredit.sol\";\n\n/// @title AgentJobV2\n/// @notice A minimal ERC-8183-style agent job with escrowed native value and an INDEPENDENT\n/// evaluator named at creation. In Predge's deployment the evaluator is the\n/// `PredgeValidatorBondV2` contract, so the evaluator's verdict is backed by a bond and\n/// moves the money in the same transaction.\n///\n/// Lifecycle:\n/// createJob (client escrows value, names provider, evaluator, spec and an expiry)\n/// submit (provider, strictly before `expiredAt`)\n/// complete / reject (evaluator, strictly before `submittedAt + evaluationWindow`)\n/// claimRefund (anyone may trigger; the CLIENT is credited) when\n/// - the job is still Open at or after `expiredAt`, or\n/// - the job is Submitted and the evaluator stayed silent until\n/// `submittedAt + evaluationWindow`.\n///\n/// Timeout policy (documented choice): if the evaluator never decides, the CLIENT is\n/// refunded, as ERC-8183 does on expiry. The alternative (pay the provider by default)\n/// would let a silent or bribed evaluator pay out unverified work. The cost of this\n/// choice is borne by the provider, who did the work; providers should only work for\n/// evaluators they trust to answer inside the window.\n///\n/// Money never leaves by push. Every payout, whether to provider or client, is credited\n/// to `credit[account]` and pulled with `withdraw` / `withdrawTo`, so a reverting or\n/// blocklisted recipient can neither force an outcome nor lock another party's funds.\n///\n/// @dev Native value only; amounts are in the chain's native wei. There is no owner, no\n/// admin, no upgrade path and no mutable parameter: `evaluationWindow` is immutable.\n/// Deliverable hashing: `sha256`. When the provider supplies the output bytes, the hash is\n/// computed on-chain with the sha256 precompile (so the stored hash provably belongs to\n/// those bytes); otherwise the provider's claimed hash is stored as given.\ncontract AgentJobV2 is IAgentJobV2, PullCredit {\n /// @notice Seconds the evaluator has to decide after a submission. Immutable, so it cannot\n /// be changed under a job that is already running.\n uint64 public immutable evaluationWindow;\n\n uint256 public jobCount;\n /// @notice Sum of every job's escrow that has not been credited yet (Open or Submitted).\n uint256 public totalEscrowed;\n\n mapping(uint256 => Job) private _jobs;\n\n event JobCreated(\n uint256 indexed jobId,\n address indexed client,\n address indexed evaluator,\n address provider,\n uint256 escrow,\n uint64 expiredAt,\n bytes32 specHash\n );\n event Submitted(uint256 indexed jobId, address indexed provider, bytes32 deliverable, bool hashedOnChain);\n event Completed(uint256 indexed jobId, address indexed evaluator, address indexed recipient, uint256 amount, bytes32 reason);\n event Rejected(uint256 indexed jobId, address indexed evaluator, address indexed recipient, uint256 amount, bytes32 reason);\n event Refunded(uint256 indexed jobId, address indexed caller, address indexed recipient, uint256 amount);\n\n error ZeroAddress();\n error ZeroEscrow();\n error BadExpiry();\n error BadWindow();\n error NotProvider();\n error NotEvaluator();\n error BadState();\n error PastDeadline();\n error NotExpired();\n error HashMismatch();\n error ZeroDeliverable();\n\n /// @param evaluationWindow_ seconds the evaluator has after a submission; must be non-zero.\n constructor(uint64 evaluationWindow_) {\n if (evaluationWindow_ == 0) revert BadWindow();\n evaluationWindow = evaluationWindow_;\n }\n\n /// @notice Client opens a job, escrows `msg.value`, and names the provider, the independent\n /// evaluator, the acceptance-test commitment and the expiry.\n /// @param provider the worker; paid on `complete`\n /// @param evaluator the only address that can complete or reject\n /// @param specHash the acceptance test commitment (the Predge `requestHash`)\n /// @param expiredAt unix time; the provider must submit strictly before it, after it the\n /// client can reclaim the escrow of a job that was never submitted\n function createJob(address provider, address evaluator, bytes32 specHash, uint64 expiredAt)\n external\n payable\n returns (uint256 jobId)\n {\n if (provider == address(0) || evaluator == address(0)) revert ZeroAddress();\n if (msg.value == 0) revert ZeroEscrow();\n if (expiredAt <= block.timestamp) revert BadExpiry();\n jobId = ++jobCount;\n totalEscrowed += msg.value;\n Job storage j = _jobs[jobId];\n j.client = msg.sender;\n j.provider = provider;\n j.evaluator = evaluator;\n j.escrow = msg.value;\n j.expiredAt = expiredAt;\n j.state = State.Open;\n j.specHash = specHash;\n emit JobCreated(jobId, msg.sender, evaluator, provider, msg.value, expiredAt, specHash);\n }\n\n /// @notice Provider submits the deliverable, strictly before `expiredAt`.\n /// @param deliverable sha256 of the output. Must be non-zero.\n /// @param output optional output bytes. If non-empty, `sha256(output)` must equal\n /// `deliverable` (reverts `HashMismatch` otherwise) and the job records that\n /// the hash was verified on-chain. If empty, the claimed hash is stored.\n function submit(uint256 jobId, bytes32 deliverable, bytes calldata output) external {\n Job storage j = _jobs[jobId];\n if (j.state != State.Open) revert BadState();\n if (msg.sender != j.provider) revert NotProvider();\n if (block.timestamp >= j.expiredAt) revert PastDeadline();\n if (deliverable == bytes32(0)) revert ZeroDeliverable();\n bool onChain = output.length != 0;\n if (onChain && sha256(output) != deliverable) revert HashMismatch();\n j.deliverable = deliverable;\n j.hashedOnChain = onChain;\n j.submittedAt = uint64(block.timestamp);\n j.state = State.Submitted;\n emit Submitted(jobId, msg.sender, deliverable, onChain);\n }\n\n /// @notice ERC-8183 `complete`. Evaluator only, while the job is Submitted and the\n /// evaluation window is still running. Credits the provider.\n /// @param reason the evaluator's attestation commitment (Predge: keccak256 of the signed verdict)\n function complete(uint256 jobId, bytes32 reason, bytes calldata) external {\n Job storage j = _decide(jobId, reason);\n j.state = State.Completed;\n uint256 amt = _release(j, j.provider);\n emit Completed(jobId, msg.sender, j.provider, amt, reason);\n }\n\n /// @notice ERC-8183 `reject`. Evaluator only, same timing as `complete`. Credits the client.\n function reject(uint256 jobId, bytes32 reason, bytes calldata) external {\n Job storage j = _decide(jobId, reason);\n j.state = State.Rejected;\n uint256 amt = _release(j, j.client);\n emit Rejected(jobId, msg.sender, j.client, amt, reason);\n }\n\n /// @notice Refund the client of a job that ran out of time. Callable by anyone (the credit\n /// always goes to the client): see the contract notes for the two cases.\n function claimRefund(uint256 jobId) external {\n Job storage j = _jobs[jobId];\n State s = j.state;\n if (s == State.Open) {\n if (block.timestamp < j.expiredAt) revert NotExpired();\n } else if (s == State.Submitted) {\n if (block.timestamp < uint256(j.submittedAt) + evaluationWindow) revert NotExpired();\n } else {\n revert BadState();\n }\n j.state = State.Expired;\n uint256 amt = _release(j, j.client);\n emit Refunded(jobId, msg.sender, j.client, amt);\n }\n\n /// @inheritdoc IAgentJobV2\n function getJob(uint256 jobId) external view returns (Job memory) {\n return _jobs[jobId];\n }\n\n function _decide(uint256 jobId, bytes32 reason) private returns (Job storage j) {\n j = _jobs[jobId];\n if (msg.sender != j.evaluator) revert NotEvaluator();\n if (j.state != State.Submitted) revert BadState();\n if (block.timestamp >= uint256(j.submittedAt) + evaluationWindow) revert PastDeadline();\n j.reason = reason;\n }\n\n function _release(Job storage j, address to) private returns (uint256 amt) {\n amt = j.escrow;\n j.escrow = 0;\n totalEscrowed -= amt;\n _addCredit(to, amt);\n }\n}\n" + }, + "v2/interfaces/IAgentJobV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @notice The slice of `AgentJobV2` that other Predge contracts read and drive.\ninterface IAgentJobV2 {\n /// @dev Forward-only, except that `Open` and `Submitted` may each end in `Expired`.\n enum State {\n None,\n Open,\n Submitted,\n Completed,\n Rejected,\n Expired\n }\n\n struct Job {\n address client; // pays; refunded on reject / expiry\n address provider; // does the work; paid on complete\n address evaluator; // the only address that can complete or reject\n uint256 escrow; // native value held for this job until it ends\n uint64 expiredAt; // the provider must submit strictly before this\n uint64 submittedAt; // 0 until submitted\n State state;\n bool hashedOnChain; // true if the deliverable hash was computed on-chain from submitted bytes\n bytes32 specHash; // the committed acceptance test (the Predge requestHash)\n bytes32 deliverable; // sha256 of the provider's output (zero until submitted)\n bytes32 reason; // the evaluator's attestation commitment (zero until decided)\n }\n\n function getJob(uint256 jobId) external view returns (Job memory);\n function complete(uint256 jobId, bytes32 reason, bytes calldata optParams) external;\n function reject(uint256 jobId, bytes32 reason, bytes calldata optParams) external;\n}\n" + }, + "v2/access/PullCredit.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title PullCredit\n/// @notice Pull-payment ledger shared by the Predge v2 contracts. Whoever is owed value is\n/// credited here and withdraws it themselves, so a recipient that reverts or is\n/// blocklisted can never force an outcome or lock anybody else's funds.\n/// @dev `totalCredit` is the sum of all outstanding credits; the inheriting contract's native\n/// balance is always at least `totalCredit` plus whatever it still escrows or bonds.\n/// Checks-effects-interactions: the credit is zeroed and the event emitted before the call.\nabstract contract PullCredit {\n /// @notice Withdrawable balance per account.\n mapping(address => uint256) public credit;\n /// @notice Sum of all credits not yet withdrawn.\n uint256 public totalCredit;\n\n event Withdrawn(address indexed account, address indexed to, uint256 amount);\n\n error NothingToWithdraw();\n error TransferFailed();\n error ZeroRecipient();\n\n /// @notice Withdraw the caller's whole credit to the caller.\n function withdraw() external {\n _withdraw(payable(msg.sender));\n }\n\n /// @notice Withdraw the caller's whole credit to `to`, e.g. when the caller's own address\n /// cannot receive value (blocklisted or a contract without a receive function).\n function withdrawTo(address payable to) external {\n if (to == address(0)) revert ZeroRecipient();\n _withdraw(to);\n }\n\n function _addCredit(address to, uint256 amount) internal {\n credit[to] += amount;\n totalCredit += amount;\n }\n\n function _withdraw(address payable to) private {\n uint256 amt = credit[msg.sender];\n if (amt == 0) revert NothingToWithdraw();\n credit[msg.sender] = 0;\n totalCredit -= amt;\n emit Withdrawn(msg.sender, to, amt);\n (bool ok,) = to.call{value: amt}(\"\");\n if (!ok) revert TransferFailed();\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/deployments/v2/verify/MockRefundProtocolV2.json b/deployments/v2/verify/MockRefundProtocolV2.json new file mode 100644 index 0000000..9121898 --- /dev/null +++ b/deployments/v2/verify/MockRefundProtocolV2.json @@ -0,0 +1,26 @@ +{ + "language": "Solidity", + "sources": { + "v2/mocks/MockRefundProtocolV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @notice DEMO / TEST ONLY stand-in for a Refund Protocol. It holds NO funds and moves NO\n/// money: `refundByArbiter` only flips a flag and emits an event, so a \"refund\" here\n/// proves the arbiter made the call and nothing more. It exposes the payment parties the\n/// v2 arbiter binds evidence to. Anyone can create a payment record; that is intended for\n/// demos and is the reason this contract must never be treated as a payment rail.\ncontract MockRefundProtocolV2 {\n struct Payment {\n address payer;\n address payee;\n uint256 amount;\n uint256 releaseTimestamp;\n bool refunded;\n }\n\n address public immutable arbiter;\n uint256 public paymentCount;\n uint256 public refundCount;\n mapping(uint256 => Payment) public payments;\n mapping(address => uint256) public lockupSeconds;\n\n event PaymentRecorded(uint256 indexed paymentID, address indexed payer, address indexed payee, uint256 amount, uint256 releaseTimestamp);\n event RefundByArbiter(uint256 indexed paymentID, address indexed caller);\n event LockupSet(address indexed recipient, uint256 lockupSeconds);\n\n error CallerNotArbiter();\n error UnknownPayment();\n error AlreadyRefunded();\n error AlreadyReleased();\n error ZeroAddress();\n\n constructor(address arbiter_) {\n if (arbiter_ == address(0)) revert ZeroAddress();\n arbiter = arbiter_;\n }\n\n /// @notice Record a payment's parties (no value moves). Demo only.\n function recordPayment(address payer, address payee, uint256 amount, uint256 releaseTimestamp)\n external\n returns (uint256 paymentID)\n {\n if (payer == address(0) || payee == address(0)) revert ZeroAddress();\n paymentID = ++paymentCount;\n payments[paymentID] = Payment(payer, payee, amount, releaseTimestamp, false);\n emit PaymentRecorded(paymentID, payer, payee, amount, releaseTimestamp);\n }\n\n function paymentInfo(uint256 paymentID)\n external\n view\n returns (address payer, address payee, uint256 amount, uint256 releaseTimestamp)\n {\n Payment storage p = payments[paymentID];\n return (p.payer, p.payee, p.amount, p.releaseTimestamp);\n }\n\n function refundByArbiter(uint256 paymentID) external {\n if (msg.sender != arbiter) revert CallerNotArbiter();\n Payment storage p = payments[paymentID];\n if (p.payee == address(0)) revert UnknownPayment();\n if (p.refunded) revert AlreadyRefunded();\n if (block.timestamp >= p.releaseTimestamp) revert AlreadyReleased();\n p.refunded = true;\n refundCount += 1;\n emit RefundByArbiter(paymentID, msg.sender);\n }\n\n function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external {\n if (msg.sender != arbiter) revert CallerNotArbiter();\n lockupSeconds[recipient] = recipientLockupSeconds;\n emit LockupSet(recipient, recipientLockupSeconds);\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/deployments/v2/verify/MockRefundProtocolV2.v2-0.json b/deployments/v2/verify/MockRefundProtocolV2.v2-0.json new file mode 100644 index 0000000..9121898 --- /dev/null +++ b/deployments/v2/verify/MockRefundProtocolV2.v2-0.json @@ -0,0 +1,26 @@ +{ + "language": "Solidity", + "sources": { + "v2/mocks/MockRefundProtocolV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @notice DEMO / TEST ONLY stand-in for a Refund Protocol. It holds NO funds and moves NO\n/// money: `refundByArbiter` only flips a flag and emits an event, so a \"refund\" here\n/// proves the arbiter made the call and nothing more. It exposes the payment parties the\n/// v2 arbiter binds evidence to. Anyone can create a payment record; that is intended for\n/// demos and is the reason this contract must never be treated as a payment rail.\ncontract MockRefundProtocolV2 {\n struct Payment {\n address payer;\n address payee;\n uint256 amount;\n uint256 releaseTimestamp;\n bool refunded;\n }\n\n address public immutable arbiter;\n uint256 public paymentCount;\n uint256 public refundCount;\n mapping(uint256 => Payment) public payments;\n mapping(address => uint256) public lockupSeconds;\n\n event PaymentRecorded(uint256 indexed paymentID, address indexed payer, address indexed payee, uint256 amount, uint256 releaseTimestamp);\n event RefundByArbiter(uint256 indexed paymentID, address indexed caller);\n event LockupSet(address indexed recipient, uint256 lockupSeconds);\n\n error CallerNotArbiter();\n error UnknownPayment();\n error AlreadyRefunded();\n error AlreadyReleased();\n error ZeroAddress();\n\n constructor(address arbiter_) {\n if (arbiter_ == address(0)) revert ZeroAddress();\n arbiter = arbiter_;\n }\n\n /// @notice Record a payment's parties (no value moves). Demo only.\n function recordPayment(address payer, address payee, uint256 amount, uint256 releaseTimestamp)\n external\n returns (uint256 paymentID)\n {\n if (payer == address(0) || payee == address(0)) revert ZeroAddress();\n paymentID = ++paymentCount;\n payments[paymentID] = Payment(payer, payee, amount, releaseTimestamp, false);\n emit PaymentRecorded(paymentID, payer, payee, amount, releaseTimestamp);\n }\n\n function paymentInfo(uint256 paymentID)\n external\n view\n returns (address payer, address payee, uint256 amount, uint256 releaseTimestamp)\n {\n Payment storage p = payments[paymentID];\n return (p.payer, p.payee, p.amount, p.releaseTimestamp);\n }\n\n function refundByArbiter(uint256 paymentID) external {\n if (msg.sender != arbiter) revert CallerNotArbiter();\n Payment storage p = payments[paymentID];\n if (p.payee == address(0)) revert UnknownPayment();\n if (p.refunded) revert AlreadyRefunded();\n if (block.timestamp >= p.releaseTimestamp) revert AlreadyReleased();\n p.refunded = true;\n refundCount += 1;\n emit RefundByArbiter(paymentID, msg.sender);\n }\n\n function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external {\n if (msg.sender != arbiter) revert CallerNotArbiter();\n lockupSeconds[recipient] = recipientLockupSeconds;\n emit LockupSet(recipient, recipientLockupSeconds);\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/deployments/v2/verify/PredgeAgentValidatorV2.json b/deployments/v2/verify/PredgeAgentValidatorV2.json new file mode 100644 index 0000000..a03acde --- /dev/null +++ b/deployments/v2/verify/PredgeAgentValidatorV2.json @@ -0,0 +1,29 @@ +{ + "language": "Solidity", + "sources": { + "v2/PredgeAgentValidatorV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Operated} from \"./access/PredgeAccess.sol\";\n\n/// @notice The one ERC-721 call the registry needs from an ERC-8004 Identity Registry.\ninterface IIdentityOwner {\n function ownerOf(uint256 agentId) external view returns (address);\n}\n\n/// @title PredgeAgentValidatorV2\n/// @notice An ERC-8004 Validation Registry shaped log of validation requests and verdicts, with\n/// one property enforced by the chain: a request is timestamped BEFORE its response, and a\n/// response is written once and never changes.\n///\n/// What this contract does NOT do, so nobody reads more into it than is there:\n/// * It does not verify the ed25519 attestation. The operator verifies it off-chain and\n/// commits `responseHash`, the keccak256 of the signed bytes.\n/// * It is not linked to `PredgeValidatorBondV2`. A registry response is a statement; the\n/// bonded verdict, checked against the on-chain evidence, is the bond's `Scored` event and the\n/// job it settled.\n/// Consumers that want the bonded one must read the bond.\n///\n/// Authorisation (fixes the v1 permissionless `validationRequest`): a request may be made\n/// by the operator, or, when an Identity Registry address was set at construction, by the\n/// current owner of the agent NFT. With no identity registry only the operator may\n/// request.\n///\n/// Domain separation: the caller supplies an acceptance-test DIGEST and the registry\n/// derives `requestHash = keccak256(abi.encode(block.chainid, address(this), agentId, digest))`.\n/// The same digest therefore yields a different request on every chain and registry, so a\n/// request or verdict cannot be replayed across deployments, and a request is keyed to its\n/// agent, so one agent cannot pre-claim another agent's digest (Olympix 5.3.3). This is the\n/// one deviation from the ERC-8004 reference signature: the fourth argument is the digest,\n/// and the derived hash is returned and emitted in the canonical event position. Use\n/// `requestHashFor` to compute it off-chain. The registry is not linked to the bond: a bonded\n/// job's specHash is `PredgeValidatorBondV2.specHashFor`, not this hash.\n///\n/// @dev Roles: `owner` (Safe) rotates the operator; `operator` (hot key) answers requests.\n/// Rotating the operator never rewrites a past verdict.\ncontract PredgeAgentValidatorV2 is Operated {\n struct Validation {\n address validator; // the operator at request time; replaced by the operator that writes the response\n uint256 agentId;\n bytes32 responseHash;\n uint64 requestedAt;\n uint64 respondedAt; // 0 while pending\n uint8 response; // 0-100\n bool hasResponse;\n string tag;\n }\n\n /// @notice Optional ERC-8004 Identity Registry (the agent NFT). Zero = only the operator requests.\n address public immutable identityRegistry;\n\n uint64 public requestCount;\n uint64 public responseCount;\n\n /// @dev `getSummary` is O(n^2) in the number of hashes (it dedupes); cap the input.\n uint256 public constant MAX_SUMMARY = 100;\n\n mapping(bytes32 => Validation) private _v;\n mapping(bytes32 => string) public requestURIOf;\n mapping(bytes32 => string) public responseURIOf;\n\n event ValidationRequest(address indexed validatorAddress, uint256 indexed agentId, string requestURI, bytes32 indexed requestHash);\n event ValidationResponse(\n address indexed validatorAddress,\n uint256 indexed agentId,\n bytes32 indexed requestHash,\n uint8 response,\n string responseURI,\n bytes32 responseHash,\n string tag\n );\n /// @notice Extra context the canonical events lack: who asked, and the digest behind the hash.\n event RequestRecorded(bytes32 indexed requestHash, bytes32 digest, address indexed requester);\n /// @notice Extra context the canonical event lacks: who answered, and when.\n event ResponseRecorded(bytes32 indexed requestHash, address indexed responder, uint64 respondedAt);\n\n error NotAuthorized();\n error WrongValidator();\n error ZeroHash();\n error AlreadyRequested();\n error NotRequested();\n error AlreadyResponded();\n error BadScore();\n error TooManyHashes();\n\n /// @param owner_ the Safe\n /// @param operator_ the hot key that answers requests\n /// @param identityRegistry_ optional ERC-8004 Identity Registry; zero disables agent-owner requests\n constructor(address owner_, address operator_, address identityRegistry_) Operated(owner_, operator_) {\n identityRegistry = identityRegistry_;\n }\n\n /// @notice The request hash this registry derives for an agent and an acceptance-test digest.\n function requestHashFor(uint256 agentId, bytes32 digest) public view returns (bytes32) {\n return keccak256(abi.encode(block.chainid, address(this), agentId, digest));\n }\n\n /// @notice Record a validation request before the outcome is knowable.\n /// @param validatorAddress must equal the current `operator` (kept for ERC-8004 compatibility)\n /// @param agentId the agent under validation (the NFT id when an identity registry is set)\n /// @param requestURI pointer to the signed acceptance test\n /// @param digest keccak256 of the signed acceptance-test bytes; must be non-zero\n /// @return requestHash the domain-separated hash under which the request is stored\n function validationRequest(address validatorAddress, uint256 agentId, string calldata requestURI, bytes32 digest)\n external\n returns (bytes32 requestHash)\n {\n if (digest == bytes32(0)) revert ZeroHash();\n if (msg.sender != operator && !_isAgentOwner(agentId)) revert NotAuthorized();\n if (validatorAddress != operator) revert WrongValidator();\n requestHash = requestHashFor(agentId, digest);\n Validation storage v = _v[requestHash];\n if (v.requestedAt != 0) revert AlreadyRequested();\n\n v.validator = validatorAddress;\n v.agentId = agentId;\n v.requestedAt = uint64(block.timestamp);\n requestURIOf[requestHash] = requestURI;\n requestCount += 1;\n\n emit ValidationRequest(validatorAddress, agentId, requestURI, requestHash);\n emit RequestRecorded(requestHash, digest, msg.sender);\n }\n\n /// @notice Record the verdict, once, for a request that exists. Operator only. The responding\n /// operator is recorded as the validator (Olympix 5.3.10), so after a rotation the canonical\n /// event and `getValidationStatus` name the key that actually answered.\n /// @param response 0-100 (Predge: 100 delivered, 0 failed, 50 void)\n function validationResponse(\n bytes32 requestHash,\n uint8 response,\n string calldata responseURI,\n bytes32 responseHash,\n string calldata tag\n ) external onlyOperator {\n if (response > 100) revert BadScore();\n if (responseHash == bytes32(0)) revert ZeroHash();\n Validation storage v = _v[requestHash];\n if (v.requestedAt == 0) revert NotRequested();\n if (v.hasResponse) revert AlreadyResponded();\n\n v.validator = msg.sender;\n v.response = response;\n v.responseHash = responseHash;\n v.respondedAt = uint64(block.timestamp);\n v.hasResponse = true;\n v.tag = tag;\n responseURIOf[requestHash] = responseURI;\n responseCount += 1;\n\n emit ValidationResponse(v.validator, v.agentId, requestHash, response, responseURI, responseHash, tag);\n emit ResponseRecorded(requestHash, msg.sender, uint64(block.timestamp));\n }\n\n /// @notice ERC-8004 `getValidationStatus`.\n function getValidationStatus(bytes32 requestHash)\n external\n view\n returns (address validatorAddress, uint256 agentId, uint8 response, bytes32 responseHash, string memory tag, uint256 lastUpdate)\n {\n Validation storage v = _v[requestHash];\n return (v.validator, v.agentId, v.response, v.responseHash, v.tag, v.respondedAt != 0 ? v.respondedAt : v.requestedAt);\n }\n\n /// @notice True once a verdict is permanently recorded.\n function isValidated(bytes32 requestHash) external view returns (bool) {\n return _v[requestHash].hasResponse;\n }\n\n /// @notice Seconds the request stayed committed before its verdict. 0 while pending, and also 0 if the\n /// verdict landed in the same block as the request (chain timestamps have block granularity), so\n /// a non-zero value proves the lead, but zero does not prove its absence.\n function commitLeadTime(bytes32 requestHash) external view returns (uint64) {\n Validation storage v = _v[requestHash];\n if (!v.hasResponse) return 0;\n return v.respondedAt - v.requestedAt;\n }\n\n /// @notice ERC-8004 `getSummary` for one agent over the supplied hashes. A hash that appears\n /// more than once is counted once.\n function getSummary(uint256 agentId, bytes32[] calldata requestHashes)\n external\n view\n returns (uint64 count, uint8 averageResponse)\n {\n uint256 n = requestHashes.length;\n if (n > MAX_SUMMARY) revert TooManyHashes();\n uint256 sum = 0;\n for (uint256 i = 0; i < n; i++) {\n bool seen = false;\n for (uint256 k = 0; k < i; k++) {\n if (requestHashes[k] == requestHashes[i]) {\n seen = true;\n break;\n }\n }\n if (seen) continue;\n Validation storage v = _v[requestHashes[i]];\n if (v.hasResponse && v.agentId == agentId) {\n sum += v.response;\n count += 1;\n }\n }\n averageResponse = count == 0 ? 0 : uint8(sum / count);\n }\n\n function _isAgentOwner(uint256 agentId) private view returns (bool) {\n if (identityRegistry == address(0)) return false;\n try IIdentityOwner(identityRegistry).ownerOf(agentId) returns (address o) {\n return o == msg.sender;\n } catch {\n return false;\n }\n }\n}\n" + }, + "v2/access/PredgeAccess.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title Owned2Step\n/// @notice Minimal two-step ownership, the same hand-over model as OpenZeppelin v5 `Ownable2Step`.\n/// The owner is passed to the constructor (the intended owner is a Safe multisig), so the\n/// deployer key never holds a role. There is deliberately NO `renounceOwnership`: a\n/// renounced owner could never rotate a compromised operator or resolve a dispute.\n/// @dev Two steps: `transferOwnership(next)` nominates, `acceptOwnership()` (called by `next`)\n/// completes. Nominating `address(0)` cancels a pending nomination. A wrong address can\n/// therefore never take ownership, because it has to prove it can sign.\nabstract contract Owned2Step {\n /// @notice Current owner. Config and dispute resolution only; never moves user funds.\n address public owner;\n /// @notice Nominated owner, or zero.\n address public pendingOwner;\n\n event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n error NotOwner();\n error NotPendingOwner();\n error ZeroAddress();\n\n modifier onlyOwner() {\n if (msg.sender != owner) revert NotOwner();\n _;\n }\n\n /// @param owner_ initial owner; must not be zero.\n constructor(address owner_) {\n if (owner_ == address(0)) revert ZeroAddress();\n owner = owner_;\n emit OwnershipTransferred(address(0), owner_);\n }\n\n /// @notice Nominate `newOwner`. Passing `address(0)` cancels a pending nomination.\n function transferOwnership(address newOwner) external onlyOwner {\n pendingOwner = newOwner;\n emit OwnershipTransferStarted(owner, newOwner);\n }\n\n /// @notice Complete the hand-over. Only the nominated address can call this.\n function acceptOwnership() external {\n if (msg.sender != pendingOwner) revert NotPendingOwner();\n emit OwnershipTransferred(owner, msg.sender);\n owner = msg.sender;\n pendingOwner = address(0);\n }\n}\n\n/// @title Operated\n/// @notice Adds the hot \"operator\" role (the day-to-day validator / arbiter key) under an\n/// `Owned2Step` owner. The owner can rotate the operator; the operator can never touch\n/// ownership or configuration.\n/// @dev Rotation is not retroactive by design: the contracts that pay out snapshot the staker\n/// address on each stake or ruling, so rotating this key never lets a new key take an old\n/// key's bond, and never strands the bond either. It can strand an in-flight VERDICT\n/// (Olympix 5.3.8): a Committed stake in `PredgeValidatorBondV2` can only be scored by the new\n/// key if it is given the old commitment's `(expected, salt)`. If it is not, the job times out,\n/// the client is refunded and the bond is forfeited to the provider. Rotate with no stake in\n/// flight, or hand the preimages over, unless the old key is compromised.\nabstract contract Operated is Owned2Step {\n /// @notice The hot operations key. Rotatable by the owner.\n address public operator;\n\n event OperatorUpdated(address indexed previousOperator, address indexed newOperator);\n\n error NotOperator();\n\n modifier onlyOperator() {\n if (msg.sender != operator) revert NotOperator();\n _;\n }\n\n /// @param owner_ initial owner (Safe); must not be zero.\n /// @param operator_ initial operator; must not be zero.\n constructor(address owner_, address operator_) Owned2Step(owner_) {\n if (operator_ == address(0)) revert ZeroAddress();\n operator = operator_;\n emit OperatorUpdated(address(0), operator_);\n }\n\n /// @notice Rotate the operator key. Owner only; the new key must be non-zero.\n function setOperator(address newOperator) external onlyOwner {\n if (newOperator == address(0)) revert ZeroAddress();\n emit OperatorUpdated(operator, newOperator);\n operator = newOperator;\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/deployments/v2/verify/PredgeAgentValidatorV2.v2-0.json b/deployments/v2/verify/PredgeAgentValidatorV2.v2-0.json new file mode 100644 index 0000000..6fc2c04 --- /dev/null +++ b/deployments/v2/verify/PredgeAgentValidatorV2.v2-0.json @@ -0,0 +1,29 @@ +{ + "language": "Solidity", + "sources": { + "v2/PredgeAgentValidatorV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Operated} from \"./access/PredgeAccess.sol\";\n\n/// @notice The one ERC-721 call the registry needs from an ERC-8004 Identity Registry.\ninterface IIdentityOwner {\n function ownerOf(uint256 agentId) external view returns (address);\n}\n\n/// @title PredgeAgentValidatorV2\n/// @notice An ERC-8004 Validation Registry shaped log of validation requests and verdicts, with\n/// one property enforced by the chain: a request is timestamped BEFORE its response, and a\n/// response is written once and never changes.\n///\n/// What this contract does NOT do, so nobody reads more into it than is there:\n/// * It does not verify the ed25519 attestation. The operator verifies it off-chain and\n/// commits `responseHash`, the keccak256 of the signed bytes.\n/// * It is not linked to `PredgeValidatorBondV2`. A registry response is a statement; the\n/// bonded, slashable verdict is the bond's `Scored` event and the job it settled.\n/// Consumers that want the bonded one must read the bond.\n///\n/// Authorisation (fixes the v1 permissionless `validationRequest`): a request may be made\n/// by the operator, or, when an Identity Registry address was set at construction, by the\n/// current owner of the agent NFT. With no identity registry only the operator may\n/// request.\n///\n/// Domain separation: the caller supplies an acceptance-test DIGEST and the registry\n/// derives `requestHash = keccak256(abi.encode(block.chainid, address(this), digest))`.\n/// The same digest therefore yields a different request on every chain and registry, so a\n/// request or verdict cannot be replayed across deployments. This is the one deviation from\n/// the ERC-8004 reference signature: the fourth argument is the digest, and the derived\n/// hash is returned and emitted in the canonical event position. Use `requestHashFor` to\n/// compute it off-chain, and use that value as the `AgentJobV2.specHash`.\n///\n/// @dev Roles: `owner` (Safe) rotates the operator; `operator` (hot key) answers requests.\n/// Rotating the operator never rewrites a past verdict.\ncontract PredgeAgentValidatorV2 is Operated {\n struct Validation {\n address validator; // the operator that was current when the request was made\n uint256 agentId;\n bytes32 responseHash;\n uint64 requestedAt;\n uint64 respondedAt; // 0 while pending\n uint8 response; // 0-100\n bool hasResponse;\n string tag;\n }\n\n /// @notice Optional ERC-8004 Identity Registry (the agent NFT). Zero = only the operator requests.\n address public immutable identityRegistry;\n\n uint64 public requestCount;\n uint64 public responseCount;\n\n /// @dev `getSummary` is O(n^2) in the number of hashes (it dedupes); cap the input.\n uint256 public constant MAX_SUMMARY = 100;\n\n mapping(bytes32 => Validation) private _v;\n mapping(bytes32 => string) public requestURIOf;\n mapping(bytes32 => string) public responseURIOf;\n\n event ValidationRequest(address indexed validatorAddress, uint256 indexed agentId, string requestURI, bytes32 indexed requestHash);\n event ValidationResponse(\n address indexed validatorAddress,\n uint256 indexed agentId,\n bytes32 indexed requestHash,\n uint8 response,\n string responseURI,\n bytes32 responseHash,\n string tag\n );\n /// @notice Extra context the canonical events lack: who asked, and the digest behind the hash.\n event RequestRecorded(bytes32 indexed requestHash, bytes32 digest, address indexed requester);\n /// @notice Extra context the canonical event lacks: who answered, and when.\n event ResponseRecorded(bytes32 indexed requestHash, address indexed responder, uint64 respondedAt);\n\n error NotAuthorized();\n error WrongValidator();\n error ZeroHash();\n error AlreadyRequested();\n error NotRequested();\n error AlreadyResponded();\n error BadScore();\n error TooManyHashes();\n\n /// @param owner_ the Safe\n /// @param operator_ the hot key that answers requests\n /// @param identityRegistry_ optional ERC-8004 Identity Registry; zero disables agent-owner requests\n constructor(address owner_, address operator_, address identityRegistry_) Operated(owner_, operator_) {\n identityRegistry = identityRegistry_;\n }\n\n /// @notice The request hash this registry derives for an acceptance-test digest.\n function requestHashFor(bytes32 digest) public view returns (bytes32) {\n return keccak256(abi.encode(block.chainid, address(this), digest));\n }\n\n /// @notice Record a validation request before the outcome is knowable.\n /// @param validatorAddress must equal the current `operator` (kept for ERC-8004 compatibility)\n /// @param agentId the agent under validation (the NFT id when an identity registry is set)\n /// @param requestURI pointer to the signed acceptance test\n /// @param digest keccak256 of the signed acceptance-test bytes; must be non-zero\n /// @return requestHash the domain-separated hash under which the request is stored\n function validationRequest(address validatorAddress, uint256 agentId, string calldata requestURI, bytes32 digest)\n external\n returns (bytes32 requestHash)\n {\n if (digest == bytes32(0)) revert ZeroHash();\n if (msg.sender != operator && !_isAgentOwner(agentId)) revert NotAuthorized();\n if (validatorAddress != operator) revert WrongValidator();\n requestHash = requestHashFor(digest);\n Validation storage v = _v[requestHash];\n if (v.requestedAt != 0) revert AlreadyRequested();\n\n v.validator = validatorAddress;\n v.agentId = agentId;\n v.requestedAt = uint64(block.timestamp);\n requestURIOf[requestHash] = requestURI;\n requestCount += 1;\n\n emit ValidationRequest(validatorAddress, agentId, requestURI, requestHash);\n emit RequestRecorded(requestHash, digest, msg.sender);\n }\n\n /// @notice Record the verdict, once, for a request that exists. Operator only.\n /// @param response 0-100 (Predge: 100 delivered, 0 failed, 50 void)\n function validationResponse(\n bytes32 requestHash,\n uint8 response,\n string calldata responseURI,\n bytes32 responseHash,\n string calldata tag\n ) external onlyOperator {\n if (response > 100) revert BadScore();\n if (responseHash == bytes32(0)) revert ZeroHash();\n Validation storage v = _v[requestHash];\n if (v.requestedAt == 0) revert NotRequested();\n if (v.hasResponse) revert AlreadyResponded();\n\n v.response = response;\n v.responseHash = responseHash;\n v.respondedAt = uint64(block.timestamp);\n v.hasResponse = true;\n v.tag = tag;\n responseURIOf[requestHash] = responseURI;\n responseCount += 1;\n\n emit ValidationResponse(v.validator, v.agentId, requestHash, response, responseURI, responseHash, tag);\n emit ResponseRecorded(requestHash, msg.sender, uint64(block.timestamp));\n }\n\n /// @notice ERC-8004 `getValidationStatus`.\n function getValidationStatus(bytes32 requestHash)\n external\n view\n returns (address validatorAddress, uint256 agentId, uint8 response, bytes32 responseHash, string memory tag, uint256 lastUpdate)\n {\n Validation storage v = _v[requestHash];\n return (v.validator, v.agentId, v.response, v.responseHash, v.tag, v.respondedAt != 0 ? v.respondedAt : v.requestedAt);\n }\n\n /// @notice True once a verdict is permanently recorded.\n function isValidated(bytes32 requestHash) external view returns (bool) {\n return _v[requestHash].hasResponse;\n }\n\n /// @notice Seconds the request stayed committed before its verdict. 0 while pending, and also 0 if the\n /// verdict landed in the same block as the request (chain timestamps have block granularity), so\n /// a non-zero value proves the lead, but zero does not prove its absence.\n function commitLeadTime(bytes32 requestHash) external view returns (uint64) {\n Validation storage v = _v[requestHash];\n if (!v.hasResponse) return 0;\n return v.respondedAt - v.requestedAt;\n }\n\n /// @notice ERC-8004 `getSummary` for one agent over the supplied hashes. A hash that appears\n /// more than once is counted once.\n function getSummary(uint256 agentId, bytes32[] calldata requestHashes)\n external\n view\n returns (uint64 count, uint8 averageResponse)\n {\n uint256 n = requestHashes.length;\n if (n > MAX_SUMMARY) revert TooManyHashes();\n uint256 sum = 0;\n for (uint256 i = 0; i < n; i++) {\n bool seen = false;\n for (uint256 k = 0; k < i; k++) {\n if (requestHashes[k] == requestHashes[i]) {\n seen = true;\n break;\n }\n }\n if (seen) continue;\n Validation storage v = _v[requestHashes[i]];\n if (v.hasResponse && v.agentId == agentId) {\n sum += v.response;\n count += 1;\n }\n }\n averageResponse = count == 0 ? 0 : uint8(sum / count);\n }\n\n function _isAgentOwner(uint256 agentId) private view returns (bool) {\n if (identityRegistry == address(0)) return false;\n try IIdentityOwner(identityRegistry).ownerOf(agentId) returns (address o) {\n return o == msg.sender;\n } catch {\n return false;\n }\n }\n}\n" + }, + "v2/access/PredgeAccess.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title Owned2Step\n/// @notice Minimal two-step ownership, the same hand-over model as OpenZeppelin v5 `Ownable2Step`.\n/// The owner is passed to the constructor (the intended owner is a Safe multisig), so the\n/// deployer key never holds a role. There is deliberately NO `renounceOwnership`: a\n/// renounced owner could never rotate a compromised operator or resolve a dispute.\n/// @dev Two steps: `transferOwnership(next)` nominates, `acceptOwnership()` (called by `next`)\n/// completes. Nominating `address(0)` cancels a pending nomination. A wrong address can\n/// therefore never take ownership, because it has to prove it can sign.\nabstract contract Owned2Step {\n /// @notice Current owner. Config and dispute resolution only; never moves user funds.\n address public owner;\n /// @notice Nominated owner, or zero.\n address public pendingOwner;\n\n event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n error NotOwner();\n error NotPendingOwner();\n error ZeroAddress();\n\n modifier onlyOwner() {\n if (msg.sender != owner) revert NotOwner();\n _;\n }\n\n /// @param owner_ initial owner; must not be zero.\n constructor(address owner_) {\n if (owner_ == address(0)) revert ZeroAddress();\n owner = owner_;\n emit OwnershipTransferred(address(0), owner_);\n }\n\n /// @notice Nominate `newOwner`. Passing `address(0)` cancels a pending nomination.\n function transferOwnership(address newOwner) external onlyOwner {\n pendingOwner = newOwner;\n emit OwnershipTransferStarted(owner, newOwner);\n }\n\n /// @notice Complete the hand-over. Only the nominated address can call this.\n function acceptOwnership() external {\n if (msg.sender != pendingOwner) revert NotPendingOwner();\n emit OwnershipTransferred(owner, msg.sender);\n owner = msg.sender;\n pendingOwner = address(0);\n }\n}\n\n/// @title Operated\n/// @notice Adds the hot \"operator\" role (the day-to-day validator / arbiter key) under an\n/// `Owned2Step` owner. The owner can rotate the operator; the operator can never touch\n/// ownership or configuration.\n/// @dev Rotation is not retroactive by design: the contracts that pay out snapshot the staker\n/// address on each stake or ruling, so rotating this key never lets a new key take an old\n/// key's funds, and never strands them either.\nabstract contract Operated is Owned2Step {\n /// @notice The hot operations key. Rotatable by the owner.\n address public operator;\n\n event OperatorUpdated(address indexed previousOperator, address indexed newOperator);\n\n error NotOperator();\n\n modifier onlyOperator() {\n if (msg.sender != operator) revert NotOperator();\n _;\n }\n\n /// @param owner_ initial owner (Safe); must not be zero.\n /// @param operator_ initial operator; must not be zero.\n constructor(address owner_, address operator_) Owned2Step(owner_) {\n if (operator_ == address(0)) revert ZeroAddress();\n operator = operator_;\n emit OperatorUpdated(address(0), operator_);\n }\n\n /// @notice Rotate the operator key. Owner only; the new key must be non-zero.\n function setOperator(address newOperator) external onlyOwner {\n if (newOperator == address(0)) revert ZeroAddress();\n emit OperatorUpdated(operator, newOperator);\n operator = newOperator;\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/deployments/v2/verify/PredgeRefundArbiterV2.json b/deployments/v2/verify/PredgeRefundArbiterV2.json new file mode 100644 index 0000000..d385dd8 --- /dev/null +++ b/deployments/v2/verify/PredgeRefundArbiterV2.json @@ -0,0 +1,32 @@ +{ + "language": "Solidity", + "sources": { + "v2/PredgeRefundArbiterV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Operated} from \"./access/PredgeAccess.sol\";\nimport {PullCredit} from \"./access/PullCredit.sol\";\n\n/// @notice What the arbiter needs from a Refund Protocol. `paymentInfo` is the parties-and-amount\n/// view the arbiter binds evidence and bond size to. Circle's production contract is not\n/// known to expose exactly this function: against it, a thin read-only adapter that\n/// implements `paymentInfo` and forwards the two write calls is required, and must be\n/// reviewed before use. On our chains only `MockRefundProtocolV2` exists.\ninterface IRefundProtocolV2 {\n /// @return payer the party refunded by `refundByArbiter`\n /// @return payee the recipient of the payment\n /// @return amount the payment amount, in the same native wei the bond is sized in\n /// @return releaseTimestamp when the payee may withdraw; a refund is impossible after it\n function paymentInfo(uint256 paymentID)\n external\n view\n returns (address payer, address payee, uint256 amount, uint256 releaseTimestamp);\n function refundByArbiter(uint256 paymentID) external;\n function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external;\n}\n\n/// @title PredgeRefundArbiterV2\n/// @notice A bonded, OPTIMISTIC arbiter for a Refund Protocol. The operator rules; nothing moves\n/// until a challenge window has passed; the harmed party can freeze the ruling and escalate\n/// it to the owner (a Safe); and a ruling the owner overturns costs the operator its bond.\n///\n/// Flow, per payment:\n/// 1. `fileEvidence`: the PAYER or the PAYEE (read from the Refund Protocol, not\n/// chosen by the caller) files the hash of its own evidence. Write-once per party, and\n/// only before a ruling exists.\n/// 2. `rule` (operator): cites one party's filed evidence, picks Refund or Release, and\n/// posts a bond of at least `max(minBond, ceil(amount * bondBps / 10000))`. The\n/// operator may not itself be a party. Nothing executes yet.\n/// 3. Within `window` seconds of the ruling, the party the ruling HARMS (the payee on a\n/// Refund, the payer on a Release) may call `dispute` with counter-evidence. That\n/// freezes the ruling and starts the owner's `RESOLUTION_TIMEOUT`.\n/// 4a. Undisputed: after the window anyone calls `finalize`. The bond is credited back to\n/// the staker and, for a Refund, `refundByArbiter` is called on the Refund Protocol.\n/// 4b. Disputed: the owner calls `resolve(paymentID, finalDirection)`. If it equals the\n/// ruling, the ruling is upheld exactly as in 4a. If not, the ruling is overturned:\n/// the bond is credited to the harmed party (the disputer), and the owner's\n/// direction is executed (a Refund is sent to the Refund Protocol).\n/// 4c. If neither happens before `expiresAt` (owner silent, or the Refund Protocol keeps\n/// reverting), anyone calls `expire`: the ruling is voided, NOTHING is executed, and\n/// the bond returns to the staker.\n///\n/// Trust, stated plainly: for disputed rulings the owner Safe is the court of last\n/// resort. An operator that rules wrongly is only punished if the harmed party notices and\n/// disputes inside the window, and the owner then agrees. The whole life of a ruling must fit\n/// before the Refund Protocol's lockup ends (Olympix 5.2.1): `rule` requires, for EITHER\n/// direction, `releaseTimestamp > now + window + RESOLUTION_TIMEOUT`, so a dispute filed at the\n/// last second still leaves the owner its full resolution period, and a refund chosen by\n/// `finalize` or `resolve` can still execute. `finalize` should be called promptly (the operator,\n/// who wants its bond back, and the payer, who wants the refund, both have a reason to): once\n/// `expiresAt` has passed anyone can `expire` the ruling instead, which executes nothing and\n/// returns the bond.\n///\n/// @dev Native value only, in the chain's native wei. The contract holds `totalBonded +\n/// totalCredit` and never user payments. Parameters are bounded and not retroactive: the\n/// window is snapshotted into each ruling, and the bond is fixed when posted.\ncontract PredgeRefundArbiterV2 is Operated, PullCredit {\n enum Direction {\n None,\n Refund,\n Release\n }\n\n enum Status {\n None,\n Ruled, // window running, or elapsed and not yet finalized\n Disputed, // frozen, waiting for the owner\n Finalized, // upheld; bond returned; refund executed if Refund (final)\n Overturned, // owner disagreed; bond paid to the harmed party (final)\n Voided // expired without a decision; nothing executed; bond returned (final)\n }\n\n struct Ruling {\n address staker; // the operator key that ruled; the only address its bond is ever returned to\n Status status;\n Direction direction;\n uint64 ruledAt;\n uint64 window; // snapshot of `challengeWindow`\n uint64 expiresAt; // after this, `expire` voids the ruling\n address evidenceFrom;\n address payer;\n address payee;\n uint256 bond;\n bytes32 evidenceHash;\n bytes32 counterEvidence;\n }\n\n uint64 public constant MIN_WINDOW = 1 hours;\n uint64 public constant MAX_WINDOW = 30 days;\n uint64 public constant RESOLUTION_TIMEOUT = 14 days;\n uint16 public constant MAX_BPS = 10_000;\n /// @notice Upper bound for a recipient lockup forwarded by `setLockupSeconds`.\n uint256 public constant MAX_LOCKUP = 365 days;\n\n IRefundProtocolV2 public immutable refundProtocol;\n\n uint64 public challengeWindow;\n uint256 public minBond;\n uint16 public bondBps;\n\n uint256 public totalBonded;\n uint64 public rulingCount;\n uint64 public overturnCount;\n\n mapping(uint256 => Ruling) private _rulings;\n /// @notice paymentID => party => hash of the evidence that party filed. Write-once.\n mapping(uint256 => mapping(address => bytes32)) public filedEvidence;\n\n event EvidenceFiled(uint256 indexed paymentID, address indexed party, bytes32 evidenceHash);\n event Ruled(\n uint256 indexed paymentID,\n address indexed staker,\n address indexed evidenceFrom,\n bytes32 evidenceHash,\n Direction direction,\n uint256 bond,\n uint64 windowEnd\n );\n event Disputed(uint256 indexed paymentID, address indexed disputer, bytes32 counterEvidence, uint64 expiresAt);\n event Finalized(uint256 indexed paymentID, address indexed staker, Direction direction, uint256 bond);\n event Overturned(uint256 indexed paymentID, address indexed harmed, Direction finalDirection, uint256 bond);\n event Voided(uint256 indexed paymentID, address indexed staker, uint256 bond);\n event ParamsUpdated(uint64 challengeWindow, uint256 minBond, uint16 bondBps);\n event LockupForwarded(address indexed recipient, uint256 lockupSeconds);\n\n error ZeroHash();\n error BadParams();\n error BadDirection();\n error UnknownPayment();\n error NotAParty();\n error OperatorIsParty();\n error EvidenceClosed();\n error EvidenceAlreadyFiled(uint256 paymentID, address party);\n error NoEvidenceFiled(uint256 paymentID, address party);\n error AlreadyRuled(uint256 paymentID);\n error BondTooSmall(uint256 required);\n error LockupTooShort();\n error BadStatus();\n error WindowOpen();\n error WindowClosed();\n error NotHarmedParty();\n error NotExpired();\n error ResolutionExpired();\n\n /// @param owner_ the Safe: resolves disputes, sets bounded parameters, rotates the operator\n /// @param operator_ the hot key that rules\n /// @param refundProtocol_ the Refund Protocol (or adapter) this arbiter drives; immutable\n /// @param challengeWindow_ seconds a ruling stays disputable, within [MIN_WINDOW, MAX_WINDOW]\n /// @param minBond_ absolute bond floor in native wei, non-zero\n /// @param bondBps_ bond as basis points of the payment amount, at most 10000\n constructor(address owner_, address operator_, address refundProtocol_, uint64 challengeWindow_, uint256 minBond_, uint16 bondBps_)\n Operated(owner_, operator_)\n {\n if (refundProtocol_ == address(0)) revert ZeroAddress();\n refundProtocol = IRefundProtocolV2(refundProtocol_);\n _setParams(challengeWindow_, minBond_, bondBps_);\n }\n\n /// @notice Owner-only, bounded, not retroactive: existing rulings keep their window and bond.\n function setParams(uint64 challengeWindow_, uint256 minBond_, uint16 bondBps_) external onlyOwner {\n _setParams(challengeWindow_, minBond_, bondBps_);\n }\n\n /// @notice Pass a recipient lockup change through to the Refund Protocol (owner only). Bounded\n /// (Olympix 5.3.12): longer than `challengeWindow + RESOLUTION_TIMEOUT`, so a ruling on a\n /// payment made under it can complete before release, and at most `MAX_LOCKUP`.\n function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external onlyOwner {\n if (recipientLockupSeconds <= uint256(challengeWindow) + RESOLUTION_TIMEOUT || recipientLockupSeconds > MAX_LOCKUP) {\n revert BadParams();\n }\n emit LockupForwarded(recipient, recipientLockupSeconds);\n refundProtocol.setLockupSeconds(recipient, recipientLockupSeconds);\n }\n\n /// @notice The bond a ruling on a payment of `amount` must post right now.\n function requiredBond(uint256 amount) public view returns (uint256) {\n uint256 scaled = (amount * bondBps + MAX_BPS - 1) / MAX_BPS;\n return scaled > minBond ? scaled : minBond;\n }\n\n /// @notice A party to the payment files the hash of its own evidence. Write-once, and only\n /// before the ruling. The caller must be the payer or the payee per the Refund Protocol.\n function fileEvidence(uint256 paymentID, bytes32 evidenceHash) external {\n if (evidenceHash == bytes32(0)) revert ZeroHash();\n if (_rulings[paymentID].status != Status.None) revert EvidenceClosed();\n (address payer, address payee,,) = refundProtocol.paymentInfo(paymentID);\n if (msg.sender != payer && msg.sender != payee) revert NotAParty();\n if (filedEvidence[paymentID][msg.sender] != bytes32(0)) revert EvidenceAlreadyFiled(paymentID, msg.sender);\n filedEvidence[paymentID][msg.sender] = evidenceHash;\n emit EvidenceFiled(paymentID, msg.sender, evidenceHash);\n }\n\n /// @notice Rule on a payment: cite a party's filed evidence, choose a direction, post the bond.\n /// Nothing executes until `finalize` or `resolve`.\n /// @param evidenceFrom the payer or payee whose filed evidence the ruling rests on\n function rule(uint256 paymentID, address evidenceFrom, bytes32 evidenceHash, Direction direction)\n external\n payable\n onlyOperator\n {\n if (evidenceHash == bytes32(0)) revert ZeroHash();\n if (direction == Direction.None) revert BadDirection();\n Ruling storage r = _rulings[paymentID];\n if (r.status != Status.None) revert AlreadyRuled(paymentID);\n\n (address payer, address payee, uint256 amount, uint256 releaseTimestamp) = refundProtocol.paymentInfo(paymentID);\n if (payee == address(0)) revert UnknownPayment();\n if (msg.sender == payer || msg.sender == payee) revert OperatorIsParty();\n if (evidenceFrom != payer && evidenceFrom != payee) revert NotAParty();\n if (filedEvidence[paymentID][evidenceFrom] != evidenceHash) revert NoEvidenceFiled(paymentID, evidenceFrom);\n uint256 required = requiredBond(amount);\n if (msg.value < required) revert BondTooSmall(required);\n uint64 window = challengeWindow;\n // The whole ruling (window, then a dispute's full resolution period) must end before the payment is\n // released, for BOTH directions: a Refund executes at finalize/resolve, and a Release can be overturned\n // into a Refund by the owner. Otherwise a free last-second dispute could run the clock out.\n if (releaseTimestamp <= block.timestamp + window + RESOLUTION_TIMEOUT) revert LockupTooShort();\n\n r.staker = msg.sender;\n r.status = Status.Ruled;\n r.direction = direction;\n r.ruledAt = uint64(block.timestamp);\n r.window = window;\n r.expiresAt = uint64(block.timestamp) + window + RESOLUTION_TIMEOUT;\n r.evidenceFrom = evidenceFrom;\n r.payer = payer;\n r.payee = payee;\n r.bond = msg.value;\n r.evidenceHash = evidenceHash;\n totalBonded += msg.value;\n rulingCount += 1;\n emit Ruled(paymentID, msg.sender, evidenceFrom, evidenceHash, direction, msg.value, uint64(block.timestamp) + window);\n }\n\n /// @notice The harmed party (payee on a Refund, payer on a Release) files counter-evidence\n /// inside the window. This freezes the ruling and hands it to the owner.\n function dispute(uint256 paymentID, bytes32 counterEvidence) external {\n if (counterEvidence == bytes32(0)) revert ZeroHash();\n Ruling storage r = _rulings[paymentID];\n if (r.status != Status.Ruled) revert BadStatus();\n if (block.timestamp >= uint256(r.ruledAt) + r.window) revert WindowClosed();\n if (msg.sender != _harmed(r)) revert NotHarmedParty();\n r.status = Status.Disputed;\n r.counterEvidence = counterEvidence;\n r.expiresAt = uint64(block.timestamp) + RESOLUTION_TIMEOUT;\n emit Disputed(paymentID, msg.sender, counterEvidence, r.expiresAt);\n }\n\n /// @notice After an undisputed window, return the bond to the staker and execute a Refund.\n /// Anyone may call. If the Refund Protocol reverts, this reverts too and the ruling\n /// can only be voided via `expire`.\n function finalize(uint256 paymentID) external {\n Ruling storage r = _rulings[paymentID];\n if (r.status != Status.Ruled) revert BadStatus();\n if (block.timestamp < uint256(r.ruledAt) + r.window) revert WindowOpen();\n _uphold(paymentID, r);\n }\n\n /// @notice Owner decides a disputed ruling. `finalDirection` equal to the ruling upholds it;\n /// any other direction overturns it and slashes the bond to the harmed party. The\n /// final direction is executed either way (a Refund calls the Refund Protocol).\n function resolve(uint256 paymentID, Direction finalDirection) external onlyOwner {\n if (finalDirection == Direction.None) revert BadDirection();\n Ruling storage r = _rulings[paymentID];\n if (r.status != Status.Disputed) revert BadStatus();\n if (block.timestamp >= r.expiresAt) revert ResolutionExpired();\n if (finalDirection == r.direction) {\n _uphold(paymentID, r);\n return;\n }\n address harmed = _harmed(r);\n uint256 bond = r.bond;\n r.status = Status.Overturned;\n r.bond = 0;\n totalBonded -= bond;\n overturnCount += 1;\n _addCredit(harmed, bond);\n emit Overturned(paymentID, harmed, finalDirection, bond);\n if (finalDirection == Direction.Refund) refundProtocol.refundByArbiter(paymentID);\n }\n\n /// @notice Void a ruling nobody decided in time. Nothing is executed; the bond returns to the\n /// staker. Anyone may call once `expiresAt` has passed.\n function expire(uint256 paymentID) external {\n Ruling storage r = _rulings[paymentID];\n if (r.status != Status.Ruled && r.status != Status.Disputed) revert BadStatus();\n if (block.timestamp < r.expiresAt) revert NotExpired();\n uint256 bond = r.bond;\n r.status = Status.Voided;\n r.bond = 0;\n totalBonded -= bond;\n _addCredit(r.staker, bond);\n emit Voided(paymentID, r.staker, bond);\n }\n\n /// @notice The full ruling record for a payment (all-zero if none).\n function rulings(uint256 paymentID) external view returns (Ruling memory) {\n return _rulings[paymentID];\n }\n\n /// @notice The party the ruling in `paymentID` would harm, or zero if there is no ruling.\n function harmedParty(uint256 paymentID) external view returns (address) {\n Ruling storage r = _rulings[paymentID];\n return r.status == Status.None ? address(0) : _harmed(r);\n }\n\n function _uphold(uint256 paymentID, Ruling storage r) private {\n uint256 bond = r.bond;\n r.status = Status.Finalized;\n r.bond = 0;\n totalBonded -= bond;\n _addCredit(r.staker, bond);\n emit Finalized(paymentID, r.staker, r.direction, bond);\n if (r.direction == Direction.Refund) refundProtocol.refundByArbiter(paymentID);\n }\n\n function _harmed(Ruling storage r) private view returns (address) {\n return r.direction == Direction.Refund ? r.payee : r.payer;\n }\n\n function _setParams(uint64 challengeWindow_, uint256 minBond_, uint16 bondBps_) private {\n if (challengeWindow_ < MIN_WINDOW || challengeWindow_ > MAX_WINDOW || minBond_ == 0 || bondBps_ > MAX_BPS)\n revert BadParams();\n challengeWindow = challengeWindow_;\n minBond = minBond_;\n bondBps = bondBps_;\n emit ParamsUpdated(challengeWindow_, minBond_, bondBps_);\n }\n}\n" + }, + "v2/access/PredgeAccess.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title Owned2Step\n/// @notice Minimal two-step ownership, the same hand-over model as OpenZeppelin v5 `Ownable2Step`.\n/// The owner is passed to the constructor (the intended owner is a Safe multisig), so the\n/// deployer key never holds a role. There is deliberately NO `renounceOwnership`: a\n/// renounced owner could never rotate a compromised operator or resolve a dispute.\n/// @dev Two steps: `transferOwnership(next)` nominates, `acceptOwnership()` (called by `next`)\n/// completes. Nominating `address(0)` cancels a pending nomination. A wrong address can\n/// therefore never take ownership, because it has to prove it can sign.\nabstract contract Owned2Step {\n /// @notice Current owner. Config and dispute resolution only; never moves user funds.\n address public owner;\n /// @notice Nominated owner, or zero.\n address public pendingOwner;\n\n event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n error NotOwner();\n error NotPendingOwner();\n error ZeroAddress();\n\n modifier onlyOwner() {\n if (msg.sender != owner) revert NotOwner();\n _;\n }\n\n /// @param owner_ initial owner; must not be zero.\n constructor(address owner_) {\n if (owner_ == address(0)) revert ZeroAddress();\n owner = owner_;\n emit OwnershipTransferred(address(0), owner_);\n }\n\n /// @notice Nominate `newOwner`. Passing `address(0)` cancels a pending nomination.\n function transferOwnership(address newOwner) external onlyOwner {\n pendingOwner = newOwner;\n emit OwnershipTransferStarted(owner, newOwner);\n }\n\n /// @notice Complete the hand-over. Only the nominated address can call this.\n function acceptOwnership() external {\n if (msg.sender != pendingOwner) revert NotPendingOwner();\n emit OwnershipTransferred(owner, msg.sender);\n owner = msg.sender;\n pendingOwner = address(0);\n }\n}\n\n/// @title Operated\n/// @notice Adds the hot \"operator\" role (the day-to-day validator / arbiter key) under an\n/// `Owned2Step` owner. The owner can rotate the operator; the operator can never touch\n/// ownership or configuration.\n/// @dev Rotation is not retroactive by design: the contracts that pay out snapshot the staker\n/// address on each stake or ruling, so rotating this key never lets a new key take an old\n/// key's bond, and never strands the bond either. It can strand an in-flight VERDICT\n/// (Olympix 5.3.8): a Committed stake in `PredgeValidatorBondV2` can only be scored by the new\n/// key if it is given the old commitment's `(expected, salt)`. If it is not, the job times out,\n/// the client is refunded and the bond is forfeited to the provider. Rotate with no stake in\n/// flight, or hand the preimages over, unless the old key is compromised.\nabstract contract Operated is Owned2Step {\n /// @notice The hot operations key. Rotatable by the owner.\n address public operator;\n\n event OperatorUpdated(address indexed previousOperator, address indexed newOperator);\n\n error NotOperator();\n\n modifier onlyOperator() {\n if (msg.sender != operator) revert NotOperator();\n _;\n }\n\n /// @param owner_ initial owner (Safe); must not be zero.\n /// @param operator_ initial operator; must not be zero.\n constructor(address owner_, address operator_) Owned2Step(owner_) {\n if (operator_ == address(0)) revert ZeroAddress();\n operator = operator_;\n emit OperatorUpdated(address(0), operator_);\n }\n\n /// @notice Rotate the operator key. Owner only; the new key must be non-zero.\n function setOperator(address newOperator) external onlyOwner {\n if (newOperator == address(0)) revert ZeroAddress();\n emit OperatorUpdated(operator, newOperator);\n operator = newOperator;\n }\n}\n" + }, + "v2/access/PullCredit.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title PullCredit\n/// @notice Pull-payment ledger shared by the Predge v2 contracts. Whoever is owed value is\n/// credited here and withdraws it themselves, so a recipient that reverts or is\n/// blocklisted can never force an outcome or lock anybody else's funds.\n/// @dev `totalCredit` is the sum of all outstanding credits; the inheriting contract's native\n/// balance is always at least `totalCredit` plus whatever it still escrows or bonds.\n/// Checks-effects-interactions: the credit is zeroed and the event emitted before the call.\nabstract contract PullCredit {\n /// @notice Withdrawable balance per account.\n mapping(address => uint256) public credit;\n /// @notice Sum of all credits not yet withdrawn.\n uint256 public totalCredit;\n\n event Withdrawn(address indexed account, address indexed to, uint256 amount);\n\n error NothingToWithdraw();\n error TransferFailed();\n error ZeroRecipient();\n\n /// @notice Withdraw the caller's whole credit to the caller.\n function withdraw() external {\n _withdraw(payable(msg.sender));\n }\n\n /// @notice Withdraw the caller's whole credit to `to`, e.g. when the caller's own address\n /// cannot receive value (blocklisted or a contract without a receive function).\n function withdrawTo(address payable to) external {\n if (to == address(0)) revert ZeroRecipient();\n _withdraw(to);\n }\n\n function _addCredit(address to, uint256 amount) internal {\n credit[to] += amount;\n totalCredit += amount;\n }\n\n function _withdraw(address payable to) private {\n uint256 amt = credit[msg.sender];\n if (amt == 0) revert NothingToWithdraw();\n credit[msg.sender] = 0;\n totalCredit -= amt;\n emit Withdrawn(msg.sender, to, amt);\n (bool ok,) = to.call{value: amt}(\"\");\n if (!ok) revert TransferFailed();\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/deployments/v2/verify/PredgeRefundArbiterV2.v2-0.json b/deployments/v2/verify/PredgeRefundArbiterV2.v2-0.json new file mode 100644 index 0000000..afdf615 --- /dev/null +++ b/deployments/v2/verify/PredgeRefundArbiterV2.v2-0.json @@ -0,0 +1,32 @@ +{ + "language": "Solidity", + "sources": { + "v2/PredgeRefundArbiterV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Operated} from \"./access/PredgeAccess.sol\";\nimport {PullCredit} from \"./access/PullCredit.sol\";\n\n/// @notice What the arbiter needs from a Refund Protocol. `paymentInfo` is the parties-and-amount\n/// view the arbiter binds evidence and bond size to. Circle's production contract is not\n/// known to expose exactly this function: against it, a thin read-only adapter that\n/// implements `paymentInfo` and forwards the two write calls is required, and must be\n/// reviewed before use. On our chains only `MockRefundProtocolV2` exists.\ninterface IRefundProtocolV2 {\n /// @return payer the party refunded by `refundByArbiter`\n /// @return payee the recipient of the payment\n /// @return amount the payment amount, in the same native wei the bond is sized in\n /// @return releaseTimestamp when the payee may withdraw; a refund is impossible after it\n function paymentInfo(uint256 paymentID)\n external\n view\n returns (address payer, address payee, uint256 amount, uint256 releaseTimestamp);\n function refundByArbiter(uint256 paymentID) external;\n function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external;\n}\n\n/// @title PredgeRefundArbiterV2\n/// @notice A bonded, OPTIMISTIC arbiter for a Refund Protocol. The operator rules; nothing moves\n/// until a challenge window has passed; the harmed party can freeze the ruling and escalate\n/// it to the owner (a Safe); and a ruling the owner overturns costs the operator its bond.\n///\n/// Flow, per payment:\n/// 1. `fileEvidence`: the PAYER or the PAYEE (read from the Refund Protocol, not\n/// chosen by the caller) files the hash of its own evidence. Write-once per party, and\n/// only before a ruling exists.\n/// 2. `rule` (operator): cites one party's filed evidence, picks Refund or Release, and\n/// posts a bond of at least `max(minBond, ceil(amount * bondBps / 10000))`. The\n/// operator may not itself be a party. Nothing executes yet.\n/// 3. Within `window` seconds of the ruling, the party the ruling HARMS (the payee on a\n/// Refund, the payer on a Release) may call `dispute` with counter-evidence. That\n/// freezes the ruling and starts the owner's `RESOLUTION_TIMEOUT`.\n/// 4a. Undisputed: after the window anyone calls `finalize`. The bond is credited back to\n/// the staker and, for a Refund, `refundByArbiter` is called on the Refund Protocol.\n/// 4b. Disputed: the owner calls `resolve(paymentID, finalDirection)`. If it equals the\n/// ruling, the ruling is upheld exactly as in 4a. If not, the ruling is overturned:\n/// the bond is credited to the harmed party (the disputer), and the owner's\n/// direction is executed (a Refund is sent to the Refund Protocol).\n/// 4c. If neither happens before `expiresAt` (owner silent, or the Refund Protocol keeps\n/// reverting), anyone calls `expire`: the ruling is voided, NOTHING is executed, and\n/// the bond returns to the staker.\n///\n/// Trust, stated plainly: for disputed rulings the owner Safe is the court of last\n/// resort. An operator that rules wrongly is only punished if the harmed party notices and\n/// disputes inside the window, and the owner then agrees. The window must close before the\n/// Refund Protocol's lockup ends, which `rule` enforces for a Refund. `finalize` should be\n/// called promptly (the operator, who wants its bond back, and the payer, who wants the refund,\n/// both have a reason to): once `expiresAt` has passed anyone can `expire` the ruling instead,\n/// which executes nothing and returns the bond.\n///\n/// @dev Native value only, in the chain's native wei. The contract holds `totalBonded +\n/// totalCredit` and never user payments. Parameters are bounded and not retroactive: the\n/// window is snapshotted into each ruling, and the bond is fixed when posted.\ncontract PredgeRefundArbiterV2 is Operated, PullCredit {\n enum Direction {\n None,\n Refund,\n Release\n }\n\n enum Status {\n None,\n Ruled, // window running, or elapsed and not yet finalized\n Disputed, // frozen, waiting for the owner\n Finalized, // upheld; bond returned; refund executed if Refund (final)\n Overturned, // owner disagreed; bond paid to the harmed party (final)\n Voided // expired without a decision; nothing executed; bond returned (final)\n }\n\n struct Ruling {\n address staker; // the operator key that ruled; the only address its bond is ever returned to\n Status status;\n Direction direction;\n uint64 ruledAt;\n uint64 window; // snapshot of `challengeWindow`\n uint64 expiresAt; // after this, `expire` voids the ruling\n address evidenceFrom;\n address payer;\n address payee;\n uint256 bond;\n bytes32 evidenceHash;\n bytes32 counterEvidence;\n }\n\n uint64 public constant MIN_WINDOW = 1 hours;\n uint64 public constant MAX_WINDOW = 30 days;\n uint64 public constant RESOLUTION_TIMEOUT = 14 days;\n uint16 public constant MAX_BPS = 10_000;\n\n IRefundProtocolV2 public immutable refundProtocol;\n\n uint64 public challengeWindow;\n uint256 public minBond;\n uint16 public bondBps;\n\n uint256 public totalBonded;\n uint64 public rulingCount;\n uint64 public overturnCount;\n\n mapping(uint256 => Ruling) private _rulings;\n /// @notice paymentID => party => hash of the evidence that party filed. Write-once.\n mapping(uint256 => mapping(address => bytes32)) public filedEvidence;\n\n event EvidenceFiled(uint256 indexed paymentID, address indexed party, bytes32 evidenceHash);\n event Ruled(\n uint256 indexed paymentID,\n address indexed staker,\n address indexed evidenceFrom,\n bytes32 evidenceHash,\n Direction direction,\n uint256 bond,\n uint64 windowEnd\n );\n event Disputed(uint256 indexed paymentID, address indexed disputer, bytes32 counterEvidence, uint64 expiresAt);\n event Finalized(uint256 indexed paymentID, address indexed staker, Direction direction, uint256 bond);\n event Overturned(uint256 indexed paymentID, address indexed harmed, Direction finalDirection, uint256 bond);\n event Voided(uint256 indexed paymentID, address indexed staker, uint256 bond);\n event ParamsUpdated(uint64 challengeWindow, uint256 minBond, uint16 bondBps);\n\n error ZeroHash();\n error BadParams();\n error BadDirection();\n error UnknownPayment();\n error NotAParty();\n error OperatorIsParty();\n error EvidenceClosed();\n error EvidenceAlreadyFiled(uint256 paymentID, address party);\n error NoEvidenceFiled(uint256 paymentID, address party);\n error AlreadyRuled(uint256 paymentID);\n error BondTooSmall(uint256 required);\n error LockupTooShort();\n error BadStatus();\n error WindowOpen();\n error WindowClosed();\n error NotHarmedParty();\n error NotExpired();\n error ResolutionExpired();\n\n /// @param owner_ the Safe: resolves disputes, sets bounded parameters, rotates the operator\n /// @param operator_ the hot key that rules\n /// @param refundProtocol_ the Refund Protocol (or adapter) this arbiter drives; immutable\n /// @param challengeWindow_ seconds a ruling stays disputable, within [MIN_WINDOW, MAX_WINDOW]\n /// @param minBond_ absolute bond floor in native wei, non-zero\n /// @param bondBps_ bond as basis points of the payment amount, at most 10000\n constructor(address owner_, address operator_, address refundProtocol_, uint64 challengeWindow_, uint256 minBond_, uint16 bondBps_)\n Operated(owner_, operator_)\n {\n if (refundProtocol_ == address(0)) revert ZeroAddress();\n refundProtocol = IRefundProtocolV2(refundProtocol_);\n _setParams(challengeWindow_, minBond_, bondBps_);\n }\n\n /// @notice Owner-only, bounded, not retroactive: existing rulings keep their window and bond.\n function setParams(uint64 challengeWindow_, uint256 minBond_, uint16 bondBps_) external onlyOwner {\n _setParams(challengeWindow_, minBond_, bondBps_);\n }\n\n /// @notice Pass a recipient lockup change through to the Refund Protocol (owner only).\n function setLockupSeconds(address recipient, uint256 recipientLockupSeconds) external onlyOwner {\n refundProtocol.setLockupSeconds(recipient, recipientLockupSeconds);\n }\n\n /// @notice The bond a ruling on a payment of `amount` must post right now.\n function requiredBond(uint256 amount) public view returns (uint256) {\n uint256 scaled = (amount * bondBps + MAX_BPS - 1) / MAX_BPS;\n return scaled > minBond ? scaled : minBond;\n }\n\n /// @notice A party to the payment files the hash of its own evidence. Write-once, and only\n /// before the ruling. The caller must be the payer or the payee per the Refund Protocol.\n function fileEvidence(uint256 paymentID, bytes32 evidenceHash) external {\n if (evidenceHash == bytes32(0)) revert ZeroHash();\n if (_rulings[paymentID].status != Status.None) revert EvidenceClosed();\n (address payer, address payee,,) = refundProtocol.paymentInfo(paymentID);\n if (msg.sender != payer && msg.sender != payee) revert NotAParty();\n if (filedEvidence[paymentID][msg.sender] != bytes32(0)) revert EvidenceAlreadyFiled(paymentID, msg.sender);\n filedEvidence[paymentID][msg.sender] = evidenceHash;\n emit EvidenceFiled(paymentID, msg.sender, evidenceHash);\n }\n\n /// @notice Rule on a payment: cite a party's filed evidence, choose a direction, post the bond.\n /// Nothing executes until `finalize` or `resolve`.\n /// @param evidenceFrom the payer or payee whose filed evidence the ruling rests on\n function rule(uint256 paymentID, address evidenceFrom, bytes32 evidenceHash, Direction direction)\n external\n payable\n onlyOperator\n {\n if (evidenceHash == bytes32(0)) revert ZeroHash();\n if (direction == Direction.None) revert BadDirection();\n Ruling storage r = _rulings[paymentID];\n if (r.status != Status.None) revert AlreadyRuled(paymentID);\n\n (address payer, address payee, uint256 amount, uint256 releaseTimestamp) = refundProtocol.paymentInfo(paymentID);\n if (payee == address(0)) revert UnknownPayment();\n if (msg.sender == payer || msg.sender == payee) revert OperatorIsParty();\n if (evidenceFrom != payer && evidenceFrom != payee) revert NotAParty();\n if (filedEvidence[paymentID][evidenceFrom] != evidenceHash) revert NoEvidenceFiled(paymentID, evidenceFrom);\n uint256 required = requiredBond(amount);\n if (msg.value < required) revert BondTooSmall(required);\n uint64 window = challengeWindow;\n // A refund must still be possible when the window closes. `finalize` must then run before the\n // payment's `releaseTimestamp`; if it does not, the Refund Protocol rejects the refund and `expire` voids the ruling.\n if (direction == Direction.Refund && releaseTimestamp <= block.timestamp + window) revert LockupTooShort();\n\n r.staker = msg.sender;\n r.status = Status.Ruled;\n r.direction = direction;\n r.ruledAt = uint64(block.timestamp);\n r.window = window;\n r.expiresAt = uint64(block.timestamp) + window + RESOLUTION_TIMEOUT;\n r.evidenceFrom = evidenceFrom;\n r.payer = payer;\n r.payee = payee;\n r.bond = msg.value;\n r.evidenceHash = evidenceHash;\n totalBonded += msg.value;\n rulingCount += 1;\n emit Ruled(paymentID, msg.sender, evidenceFrom, evidenceHash, direction, msg.value, uint64(block.timestamp) + window);\n }\n\n /// @notice The harmed party (payee on a Refund, payer on a Release) files counter-evidence\n /// inside the window. This freezes the ruling and hands it to the owner.\n function dispute(uint256 paymentID, bytes32 counterEvidence) external {\n if (counterEvidence == bytes32(0)) revert ZeroHash();\n Ruling storage r = _rulings[paymentID];\n if (r.status != Status.Ruled) revert BadStatus();\n if (block.timestamp >= uint256(r.ruledAt) + r.window) revert WindowClosed();\n if (msg.sender != _harmed(r)) revert NotHarmedParty();\n r.status = Status.Disputed;\n r.counterEvidence = counterEvidence;\n r.expiresAt = uint64(block.timestamp) + RESOLUTION_TIMEOUT;\n emit Disputed(paymentID, msg.sender, counterEvidence, r.expiresAt);\n }\n\n /// @notice After an undisputed window, return the bond to the staker and execute a Refund.\n /// Anyone may call. If the Refund Protocol reverts, this reverts too and the ruling\n /// can only be voided via `expire`.\n function finalize(uint256 paymentID) external {\n Ruling storage r = _rulings[paymentID];\n if (r.status != Status.Ruled) revert BadStatus();\n if (block.timestamp < uint256(r.ruledAt) + r.window) revert WindowOpen();\n _uphold(paymentID, r);\n }\n\n /// @notice Owner decides a disputed ruling. `finalDirection` equal to the ruling upholds it;\n /// any other direction overturns it and slashes the bond to the harmed party. The\n /// final direction is executed either way (a Refund calls the Refund Protocol).\n function resolve(uint256 paymentID, Direction finalDirection) external onlyOwner {\n if (finalDirection == Direction.None) revert BadDirection();\n Ruling storage r = _rulings[paymentID];\n if (r.status != Status.Disputed) revert BadStatus();\n if (block.timestamp >= r.expiresAt) revert ResolutionExpired();\n if (finalDirection == r.direction) {\n _uphold(paymentID, r);\n return;\n }\n address harmed = _harmed(r);\n uint256 bond = r.bond;\n r.status = Status.Overturned;\n r.bond = 0;\n totalBonded -= bond;\n overturnCount += 1;\n _addCredit(harmed, bond);\n emit Overturned(paymentID, harmed, finalDirection, bond);\n if (finalDirection == Direction.Refund) refundProtocol.refundByArbiter(paymentID);\n }\n\n /// @notice Void a ruling nobody decided in time. Nothing is executed; the bond returns to the\n /// staker. Anyone may call once `expiresAt` has passed.\n function expire(uint256 paymentID) external {\n Ruling storage r = _rulings[paymentID];\n if (r.status != Status.Ruled && r.status != Status.Disputed) revert BadStatus();\n if (block.timestamp < r.expiresAt) revert NotExpired();\n uint256 bond = r.bond;\n r.status = Status.Voided;\n r.bond = 0;\n totalBonded -= bond;\n _addCredit(r.staker, bond);\n emit Voided(paymentID, r.staker, bond);\n }\n\n /// @notice The full ruling record for a payment (all-zero if none).\n function rulings(uint256 paymentID) external view returns (Ruling memory) {\n return _rulings[paymentID];\n }\n\n /// @notice The party the ruling in `paymentID` would harm, or zero if there is no ruling.\n function harmedParty(uint256 paymentID) external view returns (address) {\n Ruling storage r = _rulings[paymentID];\n return r.status == Status.None ? address(0) : _harmed(r);\n }\n\n function _uphold(uint256 paymentID, Ruling storage r) private {\n uint256 bond = r.bond;\n r.status = Status.Finalized;\n r.bond = 0;\n totalBonded -= bond;\n _addCredit(r.staker, bond);\n emit Finalized(paymentID, r.staker, r.direction, bond);\n if (r.direction == Direction.Refund) refundProtocol.refundByArbiter(paymentID);\n }\n\n function _harmed(Ruling storage r) private view returns (address) {\n return r.direction == Direction.Refund ? r.payee : r.payer;\n }\n\n function _setParams(uint64 challengeWindow_, uint256 minBond_, uint16 bondBps_) private {\n if (challengeWindow_ < MIN_WINDOW || challengeWindow_ > MAX_WINDOW || minBond_ == 0 || bondBps_ > MAX_BPS)\n revert BadParams();\n challengeWindow = challengeWindow_;\n minBond = minBond_;\n bondBps = bondBps_;\n emit ParamsUpdated(challengeWindow_, minBond_, bondBps_);\n }\n}\n" + }, + "v2/access/PredgeAccess.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title Owned2Step\n/// @notice Minimal two-step ownership, the same hand-over model as OpenZeppelin v5 `Ownable2Step`.\n/// The owner is passed to the constructor (the intended owner is a Safe multisig), so the\n/// deployer key never holds a role. There is deliberately NO `renounceOwnership`: a\n/// renounced owner could never rotate a compromised operator or resolve a dispute.\n/// @dev Two steps: `transferOwnership(next)` nominates, `acceptOwnership()` (called by `next`)\n/// completes. Nominating `address(0)` cancels a pending nomination. A wrong address can\n/// therefore never take ownership, because it has to prove it can sign.\nabstract contract Owned2Step {\n /// @notice Current owner. Config and dispute resolution only; never moves user funds.\n address public owner;\n /// @notice Nominated owner, or zero.\n address public pendingOwner;\n\n event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n error NotOwner();\n error NotPendingOwner();\n error ZeroAddress();\n\n modifier onlyOwner() {\n if (msg.sender != owner) revert NotOwner();\n _;\n }\n\n /// @param owner_ initial owner; must not be zero.\n constructor(address owner_) {\n if (owner_ == address(0)) revert ZeroAddress();\n owner = owner_;\n emit OwnershipTransferred(address(0), owner_);\n }\n\n /// @notice Nominate `newOwner`. Passing `address(0)` cancels a pending nomination.\n function transferOwnership(address newOwner) external onlyOwner {\n pendingOwner = newOwner;\n emit OwnershipTransferStarted(owner, newOwner);\n }\n\n /// @notice Complete the hand-over. Only the nominated address can call this.\n function acceptOwnership() external {\n if (msg.sender != pendingOwner) revert NotPendingOwner();\n emit OwnershipTransferred(owner, msg.sender);\n owner = msg.sender;\n pendingOwner = address(0);\n }\n}\n\n/// @title Operated\n/// @notice Adds the hot \"operator\" role (the day-to-day validator / arbiter key) under an\n/// `Owned2Step` owner. The owner can rotate the operator; the operator can never touch\n/// ownership or configuration.\n/// @dev Rotation is not retroactive by design: the contracts that pay out snapshot the staker\n/// address on each stake or ruling, so rotating this key never lets a new key take an old\n/// key's funds, and never strands them either.\nabstract contract Operated is Owned2Step {\n /// @notice The hot operations key. Rotatable by the owner.\n address public operator;\n\n event OperatorUpdated(address indexed previousOperator, address indexed newOperator);\n\n error NotOperator();\n\n modifier onlyOperator() {\n if (msg.sender != operator) revert NotOperator();\n _;\n }\n\n /// @param owner_ initial owner (Safe); must not be zero.\n /// @param operator_ initial operator; must not be zero.\n constructor(address owner_, address operator_) Owned2Step(owner_) {\n if (operator_ == address(0)) revert ZeroAddress();\n operator = operator_;\n emit OperatorUpdated(address(0), operator_);\n }\n\n /// @notice Rotate the operator key. Owner only; the new key must be non-zero.\n function setOperator(address newOperator) external onlyOwner {\n if (newOperator == address(0)) revert ZeroAddress();\n emit OperatorUpdated(operator, newOperator);\n operator = newOperator;\n }\n}\n" + }, + "v2/access/PullCredit.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title PullCredit\n/// @notice Pull-payment ledger shared by the Predge v2 contracts. Whoever is owed value is\n/// credited here and withdraws it themselves, so a recipient that reverts or is\n/// blocklisted can never force an outcome or lock anybody else's funds.\n/// @dev `totalCredit` is the sum of all outstanding credits; the inheriting contract's native\n/// balance is always at least `totalCredit` plus whatever it still escrows or bonds.\n/// Checks-effects-interactions: the credit is zeroed and the event emitted before the call.\nabstract contract PullCredit {\n /// @notice Withdrawable balance per account.\n mapping(address => uint256) public credit;\n /// @notice Sum of all credits not yet withdrawn.\n uint256 public totalCredit;\n\n event Withdrawn(address indexed account, address indexed to, uint256 amount);\n\n error NothingToWithdraw();\n error TransferFailed();\n error ZeroRecipient();\n\n /// @notice Withdraw the caller's whole credit to the caller.\n function withdraw() external {\n _withdraw(payable(msg.sender));\n }\n\n /// @notice Withdraw the caller's whole credit to `to`, e.g. when the caller's own address\n /// cannot receive value (blocklisted or a contract without a receive function).\n function withdrawTo(address payable to) external {\n if (to == address(0)) revert ZeroRecipient();\n _withdraw(to);\n }\n\n function _addCredit(address to, uint256 amount) internal {\n credit[to] += amount;\n totalCredit += amount;\n }\n\n function _withdraw(address payable to) private {\n uint256 amt = credit[msg.sender];\n if (amt == 0) revert NothingToWithdraw();\n credit[msg.sender] = 0;\n totalCredit -= amt;\n emit Withdrawn(msg.sender, to, amt);\n (bool ok,) = to.call{value: amt}(\"\");\n if (!ok) revert TransferFailed();\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/deployments/v2/verify/PredgeSettlementV2.json b/deployments/v2/verify/PredgeSettlementV2.json new file mode 100644 index 0000000..c5b804a --- /dev/null +++ b/deployments/v2/verify/PredgeSettlementV2.json @@ -0,0 +1,29 @@ +{ + "language": "Solidity", + "sources": { + "v2/PredgeSettlementV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Owned2Step} from \"./access/PredgeAccess.sol\";\n\n/// @title PredgeSettlementV2\n/// @notice Receipt layer for Predge's pay-per-call data API: an agent pays for a route in the\n/// chain's native token and this contract emits a `Paid` receipt.\n/// @dev Amounts are in native wei of the chain (on Arc the native token is USDC with 18\n/// decimals at the protocol level; the ERC-20 interface of Arc's USDC uses 6, so never\n/// mix the two units). The owner (a Safe) withdraws accumulated payments to a non-zero\n/// address; there is no other admin power and no upgrade path. Forced value (selfdestruct\n/// or coinbase) is included in `withdraw`, so it cannot be stranded.\ncontract PredgeSettlementV2 is Owned2Step {\n /// @param payer who paid\n /// @param route keccak256 of the route id\n /// @param amount native wei paid\n /// @param timestamp block time\n /// @param meta optional request reference\n event Paid(address indexed payer, bytes32 indexed route, uint256 amount, uint256 timestamp, string meta);\n event Withdrawn(address indexed to, uint256 amount);\n\n error ZeroPayment();\n error WithdrawFailed();\n\n /// @param owner_ the Safe that may withdraw\n constructor(address owner_) Owned2Step(owner_) {}\n\n /// @notice Pay for a route; emits a `Paid` receipt.\n function payForRoute(bytes32 route, string calldata meta) external payable {\n if (msg.value == 0) revert ZeroPayment();\n emit Paid(msg.sender, route, msg.value, block.timestamp, meta);\n }\n\n /// @notice Owner withdraws the whole balance to `to`. Reverts for the zero address.\n function withdraw(address payable to) external onlyOwner {\n if (to == address(0)) revert ZeroAddress();\n uint256 bal = address(this).balance;\n emit Withdrawn(to, bal);\n (bool ok,) = to.call{value: bal}(\"\");\n if (!ok) revert WithdrawFailed();\n }\n\n /// @notice A bare transfer still produces a receipt, under the route `bytes32(\"direct\")`.\n receive() external payable {\n if (msg.value == 0) revert ZeroPayment();\n emit Paid(msg.sender, bytes32(\"direct\"), msg.value, block.timestamp, \"\");\n }\n}\n" + }, + "v2/access/PredgeAccess.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title Owned2Step\n/// @notice Minimal two-step ownership, the same hand-over model as OpenZeppelin v5 `Ownable2Step`.\n/// The owner is passed to the constructor (the intended owner is a Safe multisig), so the\n/// deployer key never holds a role. There is deliberately NO `renounceOwnership`: a\n/// renounced owner could never rotate a compromised operator or resolve a dispute.\n/// @dev Two steps: `transferOwnership(next)` nominates, `acceptOwnership()` (called by `next`)\n/// completes. Nominating `address(0)` cancels a pending nomination. A wrong address can\n/// therefore never take ownership, because it has to prove it can sign.\nabstract contract Owned2Step {\n /// @notice Current owner. Config and dispute resolution only; never moves user funds.\n address public owner;\n /// @notice Nominated owner, or zero.\n address public pendingOwner;\n\n event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n error NotOwner();\n error NotPendingOwner();\n error ZeroAddress();\n\n modifier onlyOwner() {\n if (msg.sender != owner) revert NotOwner();\n _;\n }\n\n /// @param owner_ initial owner; must not be zero.\n constructor(address owner_) {\n if (owner_ == address(0)) revert ZeroAddress();\n owner = owner_;\n emit OwnershipTransferred(address(0), owner_);\n }\n\n /// @notice Nominate `newOwner`. Passing `address(0)` cancels a pending nomination.\n function transferOwnership(address newOwner) external onlyOwner {\n pendingOwner = newOwner;\n emit OwnershipTransferStarted(owner, newOwner);\n }\n\n /// @notice Complete the hand-over. Only the nominated address can call this.\n function acceptOwnership() external {\n if (msg.sender != pendingOwner) revert NotPendingOwner();\n emit OwnershipTransferred(owner, msg.sender);\n owner = msg.sender;\n pendingOwner = address(0);\n }\n}\n\n/// @title Operated\n/// @notice Adds the hot \"operator\" role (the day-to-day validator / arbiter key) under an\n/// `Owned2Step` owner. The owner can rotate the operator; the operator can never touch\n/// ownership or configuration.\n/// @dev Rotation is not retroactive by design: the contracts that pay out snapshot the staker\n/// address on each stake or ruling, so rotating this key never lets a new key take an old\n/// key's bond, and never strands the bond either. It can strand an in-flight VERDICT\n/// (Olympix 5.3.8): a Committed stake in `PredgeValidatorBondV2` can only be scored by the new\n/// key if it is given the old commitment's `(expected, salt)`. If it is not, the job times out,\n/// the client is refunded and the bond is forfeited to the provider. Rotate with no stake in\n/// flight, or hand the preimages over, unless the old key is compromised.\nabstract contract Operated is Owned2Step {\n /// @notice The hot operations key. Rotatable by the owner.\n address public operator;\n\n event OperatorUpdated(address indexed previousOperator, address indexed newOperator);\n\n error NotOperator();\n\n modifier onlyOperator() {\n if (msg.sender != operator) revert NotOperator();\n _;\n }\n\n /// @param owner_ initial owner (Safe); must not be zero.\n /// @param operator_ initial operator; must not be zero.\n constructor(address owner_, address operator_) Owned2Step(owner_) {\n if (operator_ == address(0)) revert ZeroAddress();\n operator = operator_;\n emit OperatorUpdated(address(0), operator_);\n }\n\n /// @notice Rotate the operator key. Owner only; the new key must be non-zero.\n function setOperator(address newOperator) external onlyOwner {\n if (newOperator == address(0)) revert ZeroAddress();\n emit OperatorUpdated(operator, newOperator);\n operator = newOperator;\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/deployments/v2/verify/PredgeSettlementV2.v2-0.json b/deployments/v2/verify/PredgeSettlementV2.v2-0.json new file mode 100644 index 0000000..17e226a --- /dev/null +++ b/deployments/v2/verify/PredgeSettlementV2.v2-0.json @@ -0,0 +1,29 @@ +{ + "language": "Solidity", + "sources": { + "v2/PredgeSettlementV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Owned2Step} from \"./access/PredgeAccess.sol\";\n\n/// @title PredgeSettlementV2\n/// @notice Receipt layer for Predge's pay-per-call data API: an agent pays for a route in the\n/// chain's native token and this contract emits a `Paid` receipt.\n/// @dev Amounts are in native wei of the chain (on Arc the native token is USDC with 18\n/// decimals at the protocol level; the ERC-20 interface of Arc's USDC uses 6, so never\n/// mix the two units). The owner (a Safe) withdraws accumulated payments to a non-zero\n/// address; there is no other admin power and no upgrade path. Forced value (selfdestruct\n/// or coinbase) is included in `withdraw`, so it cannot be stranded.\ncontract PredgeSettlementV2 is Owned2Step {\n /// @param payer who paid\n /// @param route keccak256 of the route id\n /// @param amount native wei paid\n /// @param timestamp block time\n /// @param meta optional request reference\n event Paid(address indexed payer, bytes32 indexed route, uint256 amount, uint256 timestamp, string meta);\n event Withdrawn(address indexed to, uint256 amount);\n\n error ZeroPayment();\n error WithdrawFailed();\n\n /// @param owner_ the Safe that may withdraw\n constructor(address owner_) Owned2Step(owner_) {}\n\n /// @notice Pay for a route; emits a `Paid` receipt.\n function payForRoute(bytes32 route, string calldata meta) external payable {\n if (msg.value == 0) revert ZeroPayment();\n emit Paid(msg.sender, route, msg.value, block.timestamp, meta);\n }\n\n /// @notice Owner withdraws the whole balance to `to`. Reverts for the zero address.\n function withdraw(address payable to) external onlyOwner {\n if (to == address(0)) revert ZeroAddress();\n uint256 bal = address(this).balance;\n emit Withdrawn(to, bal);\n (bool ok,) = to.call{value: bal}(\"\");\n if (!ok) revert WithdrawFailed();\n }\n\n /// @notice A bare transfer still produces a receipt, under the route `bytes32(\"direct\")`.\n receive() external payable {\n if (msg.value == 0) revert ZeroPayment();\n emit Paid(msg.sender, bytes32(\"direct\"), msg.value, block.timestamp, \"\");\n }\n}\n" + }, + "v2/access/PredgeAccess.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title Owned2Step\n/// @notice Minimal two-step ownership, the same hand-over model as OpenZeppelin v5 `Ownable2Step`.\n/// The owner is passed to the constructor (the intended owner is a Safe multisig), so the\n/// deployer key never holds a role. There is deliberately NO `renounceOwnership`: a\n/// renounced owner could never rotate a compromised operator or resolve a dispute.\n/// @dev Two steps: `transferOwnership(next)` nominates, `acceptOwnership()` (called by `next`)\n/// completes. Nominating `address(0)` cancels a pending nomination. A wrong address can\n/// therefore never take ownership, because it has to prove it can sign.\nabstract contract Owned2Step {\n /// @notice Current owner. Config and dispute resolution only; never moves user funds.\n address public owner;\n /// @notice Nominated owner, or zero.\n address public pendingOwner;\n\n event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n error NotOwner();\n error NotPendingOwner();\n error ZeroAddress();\n\n modifier onlyOwner() {\n if (msg.sender != owner) revert NotOwner();\n _;\n }\n\n /// @param owner_ initial owner; must not be zero.\n constructor(address owner_) {\n if (owner_ == address(0)) revert ZeroAddress();\n owner = owner_;\n emit OwnershipTransferred(address(0), owner_);\n }\n\n /// @notice Nominate `newOwner`. Passing `address(0)` cancels a pending nomination.\n function transferOwnership(address newOwner) external onlyOwner {\n pendingOwner = newOwner;\n emit OwnershipTransferStarted(owner, newOwner);\n }\n\n /// @notice Complete the hand-over. Only the nominated address can call this.\n function acceptOwnership() external {\n if (msg.sender != pendingOwner) revert NotPendingOwner();\n emit OwnershipTransferred(owner, msg.sender);\n owner = msg.sender;\n pendingOwner = address(0);\n }\n}\n\n/// @title Operated\n/// @notice Adds the hot \"operator\" role (the day-to-day validator / arbiter key) under an\n/// `Owned2Step` owner. The owner can rotate the operator; the operator can never touch\n/// ownership or configuration.\n/// @dev Rotation is not retroactive by design: the contracts that pay out snapshot the staker\n/// address on each stake or ruling, so rotating this key never lets a new key take an old\n/// key's funds, and never strands them either.\nabstract contract Operated is Owned2Step {\n /// @notice The hot operations key. Rotatable by the owner.\n address public operator;\n\n event OperatorUpdated(address indexed previousOperator, address indexed newOperator);\n\n error NotOperator();\n\n modifier onlyOperator() {\n if (msg.sender != operator) revert NotOperator();\n _;\n }\n\n /// @param owner_ initial owner (Safe); must not be zero.\n /// @param operator_ initial operator; must not be zero.\n constructor(address owner_, address operator_) Owned2Step(owner_) {\n if (operator_ == address(0)) revert ZeroAddress();\n operator = operator_;\n emit OperatorUpdated(address(0), operator_);\n }\n\n /// @notice Rotate the operator key. Owner only; the new key must be non-zero.\n function setOperator(address newOperator) external onlyOwner {\n if (newOperator == address(0)) revert ZeroAddress();\n emit OperatorUpdated(operator, newOperator);\n operator = newOperator;\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/deployments/v2/verify/PredgeValidatorBondV2.json b/deployments/v2/verify/PredgeValidatorBondV2.json new file mode 100644 index 0000000..c4d17a9 --- /dev/null +++ b/deployments/v2/verify/PredgeValidatorBondV2.json @@ -0,0 +1,35 @@ +{ + "language": "Solidity", + "sources": { + "v2/PredgeValidatorBondV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Operated} from \"./access/PredgeAccess.sol\";\nimport {PullCredit} from \"./access/PullCredit.sol\";\nimport {IAgentJobV2} from \"./interfaces/IAgentJobV2.sol\";\n\n/// @title PredgeValidatorBondV2\n/// @notice Capital behind a verdict, and the EVALUATOR of an `AgentJobV2`. The job names this\n/// contract as its evaluator, so the verdict recorded here is the thing that moves the\n/// escrow.\n///\n/// The acceptance test is deterministic: the job's deliverable counts as DELIVERED only if the\n/// provider submitted the output bytes (so the job computed `sha256(output)` on-chain,\n/// `hashedOnChain == true`) and that hash equals, by RAW EQUALITY, the `expected` value the\n/// CLIENT fixed when it created the job. A hash the provider merely claimed, with no bytes, never\n/// counts as delivered. Nothing is hashed again here.\n///\n/// A FALSE VERDICT CANNOT BE RECORDED (owner decision 23.10.2026, option a). Both sides of the\n/// comparison are on-chain at `recordScore`: the hash the chain computed and the `expected` the\n/// client committed to. So `recordScore` checks the score against them and reverts\n/// `VerdictContradictsEvidence` unless score 100 <=> delivered and score 0 <=> not delivered. This\n/// holds for any bond size, including `bondBps = 0`, and needs no challenger, no watcher and no\n/// dispute window: there is nothing to challenge, so there is no `challenge` and the bond is\n/// credited back to the staker in the same transaction as the verdict.\n///\n/// Who fixes `expected` (Olympix 5.1.1): the client. A job evaluated by this contract must have\n/// `specHash == specHashFor(keccak256(abi.encode(expected, salt)))`, which the client chooses at\n/// `createJob`. The operator learns `(expected, salt)` from the client off-chain, checks it against\n/// the job before staking, and can only commit to that value.\n///\n/// Flow:\n/// 1. `stakeAndCommit(requestHash, commitment, jobId)` (operator). Allowed ONLY while\n/// the job is Open, i.e. before any deliverable exists. `commitment` is\n/// `keccak256(abi.encode(expected, salt))` and must be the one the client bound into the\n/// job's specHash (`requestHash == specHashFor(commitment)`). The bond must be at least\n/// `max(minBond, bondBps * escrow / 10000)`.\n/// 2. The provider submits to the job (with the output bytes, or the job can only fail).\n/// 3. `recordScore(requestHash, score, expected, salt, reason)` (operator). Requires the\n/// job to be Submitted, the reveal to match the commitment, and the score to agree with\n/// the on-chain evidence. It then calls `complete` (score 100) or `reject` (score 0) on\n/// the job, so the verdict and the money move together, and credits the bond back to the\n/// staker.\n/// 4. `release(requestHash)` (anyone) if the job expired with no verdict. If the provider never\n/// submitted, nothing was owed and the bond goes back to the staker. If the provider DID submit\n/// and the operator stayed silent until the evaluation window closed (Olympix 5.2.2), the\n/// bond is FORFEITED to the provider: silence on submitted work is not free.\n///\n/// What the bond is for, after option (a): LIVENESS, not correctness. Correctness is enforced by\n/// `recordScore`. The bond is what the operator loses if it stays silent on a submitted job.\n///\n/// Honest limits, stated so they are not mistaken for guarantees:\n/// * The client defines `expected`. A provider should obtain `(expected, salt)` from the client\n/// and check it against the job's specHash before working; knowing `expected` does not\n/// let anyone pass without the real bytes, because only an on-chain hash counts.\n/// * A pass needs the output bytes in calldata; large outputs cost the provider gas.\n/// * On silence the provider receives the bond, not the escrow: the job's timeout policy\n/// still refunds the escrow to the client. With `bondBps = 0` the bond is a flat floor.\n/// * The operator's capital is locked until the job ends. A client picks the job's\n/// `expiredAt`, so an operator should read it before staking.\n/// * Only the deterministic `sha256(deliverable) == expected` test is covered. Subjective\n/// or \"void\" outcomes are not bonded.\n///\n/// @dev Native value only, in the chain's native wei. Roles: `owner` (Safe) sets bounded\n/// parameters and rotates `operator`; `operator` (hot key) stakes and scores. Parameters\n/// are not retroactive: the bond amount is fixed when staked. Payouts are pull-based. The\n/// contract holds, at all times, at least `totalBonded + totalCredit`.\ncontract PredgeValidatorBondV2 is Operated, PullCredit {\n enum Status {\n None,\n Committed, // bond posted, no verdict yet\n Scored, // verdict recorded (it matches the evidence), bond credited back to the staker (final)\n Released, // job expired before any submission, bond credited back to the staker (final)\n Forfeited // job expired after a submission with no verdict, bond credited to the provider (final)\n }\n\n struct Stake {\n address staker; // the operator key that posted the bond; the only party it is ever paid back to\n Status status;\n uint8 score; // 100 delivered / 0 not delivered, set when scored\n uint64 stakedAt;\n uint64 scoredAt; // set when scored; always >= the job's submittedAt\n uint256 jobId;\n uint256 bond; // the posted bond while Committed; zero once closed\n bytes32 commitment; // keccak256(abi.encode(expected, salt))\n bytes32 expected; // zero until revealed at recordScore\n }\n\n uint16 public constant MAX_BPS = 10_000;\n\n IAgentJobV2 public immutable job;\n\n /// @notice Absolute floor for a bond, in native wei. Applies to stakes made after a change.\n uint256 public minBond;\n /// @notice Bond as basis points of the job escrow (10000 = 100%). Applies to future stakes.\n uint16 public bondBps;\n\n /// @notice Sum of bonds currently held (Committed).\n uint256 public totalBonded;\n\n mapping(bytes32 => Stake) public stakes;\n\n event Committed(bytes32 indexed requestHash, uint256 indexed jobId, address indexed staker, bytes32 commitment, uint256 bond);\n event Scored(bytes32 indexed requestHash, uint256 indexed jobId, uint8 score, bytes32 expected, address staker, uint256 bond);\n event Released(bytes32 indexed requestHash, address indexed staker, uint256 bond);\n event Forfeited(bytes32 indexed requestHash, address indexed provider, uint256 bond);\n event ParamsUpdated(uint256 minBond, uint16 bondBps);\n\n error ZeroHash();\n error BadParams();\n error AlreadyCommitted();\n error NotCommitted();\n error BadStatus();\n error NotOpen();\n error NotSubmitted();\n error JobNotBound();\n error ProviderIsOperator();\n error ClientIsOperator();\n error CommitmentNotBound();\n error BondTooSmall(uint256 required);\n error BadScore();\n error BadReveal();\n error VerdictContradictsEvidence();\n error JobNotExpired();\n\n /// @param owner_ the Safe: sets bounded parameters, rotates the operator\n /// @param operator_ the hot key that stakes and scores\n /// @param job_ the `AgentJobV2` this bond evaluates; immutable\n /// @param minBond_ absolute floor for a bond, non-zero\n /// @param bondBps_ bond as basis points of escrow, at most 10000\n constructor(address owner_, address operator_, address job_, uint256 minBond_, uint16 bondBps_)\n Operated(owner_, operator_)\n {\n if (job_ == address(0)) revert ZeroAddress();\n job = IAgentJobV2(job_);\n _setParams(minBond_, bondBps_);\n }\n\n /// @notice Owner-only parameter change, bounded and NOT retroactive: existing stakes keep\n /// the bond they were made with.\n function setParams(uint256 minBond_, uint16 bondBps_) external onlyOwner {\n _setParams(minBond_, bondBps_);\n }\n\n /// @notice The bond a stake against `escrow` must post right now: `max(minBond, ceil(escrow * bondBps / 10000))`.\n function requiredBond(uint256 escrow) public view returns (uint256) {\n uint256 scaled = (escrow * bondBps + MAX_BPS - 1) / MAX_BPS;\n return scaled > minBond ? scaled : minBond;\n }\n\n /// @notice The specHash a client must give a job evaluated by this contract, for a commitment\n /// `keccak256(abi.encode(expected, salt))`. Domain separated by chain and by this contract.\n function specHashFor(bytes32 commitment) public view returns (bytes32) {\n return keccak256(abi.encode(block.chainid, address(this), commitment));\n }\n\n /// @notice The only verdict `recordScore` accepts for job `jobId` against `expected`: 100 if the\n /// provider submitted the bytes and their on-chain sha256 equals `expected`, otherwise 0.\n /// Meaningful once the job is Submitted.\n function verdictFor(uint256 jobId, bytes32 expected) public view returns (uint8) {\n IAgentJobV2.Job memory j = job.getJob(jobId);\n return _delivered(j, expected) ? 100 : 0;\n }\n\n /// @notice Post the bond and commit the hidden acceptance test, while the job is still Open.\n /// @param requestHash the job's `specHash`; must equal `specHashFor(commitment)`\n /// @param commitment `keccak256(abi.encode(expected, salt))` as fixed by the client in the specHash\n /// @param jobId the job this verdict will settle; this contract must be its evaluator\n function stakeAndCommit(bytes32 requestHash, bytes32 commitment, uint256 jobId) external payable onlyOperator {\n if (requestHash == bytes32(0) || commitment == bytes32(0)) revert ZeroHash();\n Stake storage s = stakes[requestHash];\n if (s.status != Status.None) revert AlreadyCommitted();\n // The client fixed `expected` (hidden behind the salt) in the specHash: the operator cannot pick its own.\n if (requestHash != specHashFor(commitment)) revert CommitmentNotBound();\n\n IAgentJobV2.Job memory j = job.getJob(jobId);\n // Only while Open and unexpired: the stake must exist before the work does.\n if (j.state != IAgentJobV2.State.Open || block.timestamp >= j.expiredAt) revert NotOpen();\n if (j.specHash != requestHash || j.evaluator != address(this)) revert JobNotBound();\n // A provider that is also the operator would decide whether to deliver and when to stay silent.\n if (j.provider == msg.sender) revert ProviderIsOperator();\n // A client that is also the operator would judge its own job (Olympix 5.2.3).\n if (j.client == msg.sender) revert ClientIsOperator();\n uint256 required = requiredBond(j.escrow);\n if (msg.value < required) revert BondTooSmall(required);\n\n s.staker = msg.sender;\n s.status = Status.Committed;\n s.stakedAt = uint64(block.timestamp);\n s.jobId = jobId;\n s.bond = msg.value;\n s.commitment = commitment;\n totalBonded += msg.value;\n emit Committed(requestHash, jobId, msg.sender, commitment, msg.value);\n }\n\n /// @notice Reveal the committed test, record the verdict, settle the job with it, and credit the\n /// bond back to the staker. Reverts `VerdictContradictsEvidence` on a false verdict.\n /// @param score 100 = delivered (job `complete`s, provider is credited), 0 = not delivered\n /// (job `reject`s, client is credited). Must equal `verdictFor(jobId, expected)`.\n /// @param expected,salt the preimage of the commitment\n /// @param reason the attestation commitment passed to the job (Predge: keccak256 of the signed verdict)\n function recordScore(bytes32 requestHash, uint8 score, bytes32 expected, bytes32 salt, bytes32 reason)\n external\n onlyOperator\n {\n if (score != 0 && score != 100) revert BadScore();\n Stake storage s = stakes[requestHash];\n if (s.status == Status.None) revert NotCommitted();\n if (s.status != Status.Committed) revert BadStatus();\n if (keccak256(abi.encode(expected, salt)) != s.commitment) revert BadReveal();\n // Re-checked here, not only at stake time: after an operator rotation the NEW operator must not be able\n // to score a job it is a party to, while inheriting the old operator's stake.\n IAgentJobV2.Job memory j = job.getJob(s.jobId);\n if (j.provider == msg.sender) revert ProviderIsOperator();\n if (j.client == msg.sender) revert ClientIsOperator();\n // Scoring needs a submission: the verdict is judged against a deliverable that exists.\n if (j.state != IAgentJobV2.State.Submitted) revert NotSubmitted();\n // Option (a): the evidence decides. A pass needs an on-chain hash equal to `expected`; a fail needs the opposite.\n if ((score == 100) != _delivered(j, expected)) revert VerdictContradictsEvidence();\n\n s.expected = expected;\n s.score = score;\n s.scoredAt = uint64(block.timestamp);\n uint256 bond = _close(s, Status.Scored, s.staker);\n emit Scored(requestHash, s.jobId, score, expected, s.staker, bond);\n\n if (score == 100) job.complete(s.jobId, reason, \"\");\n else job.reject(s.jobId, reason, \"\");\n }\n\n /// @notice Close a stake whose job expired without a verdict. Anyone may trigger it. If the provider\n /// never submitted, the bond is credited back to the staker. If the provider submitted and the\n /// operator let the evaluation window close without scoring, the bond is credited to the\n /// provider instead.\n function release(bytes32 requestHash) external {\n Stake storage s = stakes[requestHash];\n if (s.status == Status.None) revert NotCommitted();\n if (s.status != Status.Committed) revert BadStatus();\n IAgentJobV2.Job memory j = job.getJob(s.jobId);\n if (j.state != IAgentJobV2.State.Expired) revert JobNotExpired();\n if (j.submittedAt == 0) {\n uint256 bond = _close(s, Status.Released, s.staker);\n emit Released(requestHash, s.staker, bond);\n } else {\n uint256 bond = _close(s, Status.Forfeited, j.provider);\n emit Forfeited(requestHash, j.provider, bond);\n }\n }\n\n /// @dev Delivered means: the bytes were hashed on-chain AND the hash equals the client's `expected`.\n function _delivered(IAgentJobV2.Job memory j, bytes32 expected) private pure returns (bool) {\n return j.hashedOnChain && j.deliverable == expected;\n }\n\n function _close(Stake storage s, Status to, address payee) private returns (uint256 bond) {\n bond = s.bond;\n s.status = to;\n s.bond = 0;\n totalBonded -= bond;\n _addCredit(payee, bond);\n }\n\n function _setParams(uint256 minBond_, uint16 bondBps_) private {\n if (minBond_ == 0 || bondBps_ > MAX_BPS) revert BadParams();\n minBond = minBond_;\n bondBps = bondBps_;\n emit ParamsUpdated(minBond_, bondBps_);\n }\n}\n" + }, + "v2/access/PredgeAccess.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title Owned2Step\n/// @notice Minimal two-step ownership, the same hand-over model as OpenZeppelin v5 `Ownable2Step`.\n/// The owner is passed to the constructor (the intended owner is a Safe multisig), so the\n/// deployer key never holds a role. There is deliberately NO `renounceOwnership`: a\n/// renounced owner could never rotate a compromised operator or resolve a dispute.\n/// @dev Two steps: `transferOwnership(next)` nominates, `acceptOwnership()` (called by `next`)\n/// completes. Nominating `address(0)` cancels a pending nomination. A wrong address can\n/// therefore never take ownership, because it has to prove it can sign.\nabstract contract Owned2Step {\n /// @notice Current owner. Config and dispute resolution only; never moves user funds.\n address public owner;\n /// @notice Nominated owner, or zero.\n address public pendingOwner;\n\n event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n error NotOwner();\n error NotPendingOwner();\n error ZeroAddress();\n\n modifier onlyOwner() {\n if (msg.sender != owner) revert NotOwner();\n _;\n }\n\n /// @param owner_ initial owner; must not be zero.\n constructor(address owner_) {\n if (owner_ == address(0)) revert ZeroAddress();\n owner = owner_;\n emit OwnershipTransferred(address(0), owner_);\n }\n\n /// @notice Nominate `newOwner`. Passing `address(0)` cancels a pending nomination.\n function transferOwnership(address newOwner) external onlyOwner {\n pendingOwner = newOwner;\n emit OwnershipTransferStarted(owner, newOwner);\n }\n\n /// @notice Complete the hand-over. Only the nominated address can call this.\n function acceptOwnership() external {\n if (msg.sender != pendingOwner) revert NotPendingOwner();\n emit OwnershipTransferred(owner, msg.sender);\n owner = msg.sender;\n pendingOwner = address(0);\n }\n}\n\n/// @title Operated\n/// @notice Adds the hot \"operator\" role (the day-to-day validator / arbiter key) under an\n/// `Owned2Step` owner. The owner can rotate the operator; the operator can never touch\n/// ownership or configuration.\n/// @dev Rotation is not retroactive by design: the contracts that pay out snapshot the staker\n/// address on each stake or ruling, so rotating this key never lets a new key take an old\n/// key's bond, and never strands the bond either. It can strand an in-flight VERDICT\n/// (Olympix 5.3.8): a Committed stake in `PredgeValidatorBondV2` can only be scored by the new\n/// key if it is given the old commitment's `(expected, salt)`. If it is not, the job times out,\n/// the client is refunded and the bond is forfeited to the provider. Rotate with no stake in\n/// flight, or hand the preimages over, unless the old key is compromised.\nabstract contract Operated is Owned2Step {\n /// @notice The hot operations key. Rotatable by the owner.\n address public operator;\n\n event OperatorUpdated(address indexed previousOperator, address indexed newOperator);\n\n error NotOperator();\n\n modifier onlyOperator() {\n if (msg.sender != operator) revert NotOperator();\n _;\n }\n\n /// @param owner_ initial owner (Safe); must not be zero.\n /// @param operator_ initial operator; must not be zero.\n constructor(address owner_, address operator_) Owned2Step(owner_) {\n if (operator_ == address(0)) revert ZeroAddress();\n operator = operator_;\n emit OperatorUpdated(address(0), operator_);\n }\n\n /// @notice Rotate the operator key. Owner only; the new key must be non-zero.\n function setOperator(address newOperator) external onlyOwner {\n if (newOperator == address(0)) revert ZeroAddress();\n emit OperatorUpdated(operator, newOperator);\n operator = newOperator;\n }\n}\n" + }, + "v2/access/PullCredit.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title PullCredit\n/// @notice Pull-payment ledger shared by the Predge v2 contracts. Whoever is owed value is\n/// credited here and withdraws it themselves, so a recipient that reverts or is\n/// blocklisted can never force an outcome or lock anybody else's funds.\n/// @dev `totalCredit` is the sum of all outstanding credits; the inheriting contract's native\n/// balance is always at least `totalCredit` plus whatever it still escrows or bonds.\n/// Checks-effects-interactions: the credit is zeroed and the event emitted before the call.\nabstract contract PullCredit {\n /// @notice Withdrawable balance per account.\n mapping(address => uint256) public credit;\n /// @notice Sum of all credits not yet withdrawn.\n uint256 public totalCredit;\n\n event Withdrawn(address indexed account, address indexed to, uint256 amount);\n\n error NothingToWithdraw();\n error TransferFailed();\n error ZeroRecipient();\n\n /// @notice Withdraw the caller's whole credit to the caller.\n function withdraw() external {\n _withdraw(payable(msg.sender));\n }\n\n /// @notice Withdraw the caller's whole credit to `to`, e.g. when the caller's own address\n /// cannot receive value (blocklisted or a contract without a receive function).\n function withdrawTo(address payable to) external {\n if (to == address(0)) revert ZeroRecipient();\n _withdraw(to);\n }\n\n function _addCredit(address to, uint256 amount) internal {\n credit[to] += amount;\n totalCredit += amount;\n }\n\n function _withdraw(address payable to) private {\n uint256 amt = credit[msg.sender];\n if (amt == 0) revert NothingToWithdraw();\n credit[msg.sender] = 0;\n totalCredit -= amt;\n emit Withdrawn(msg.sender, to, amt);\n (bool ok,) = to.call{value: amt}(\"\");\n if (!ok) revert TransferFailed();\n }\n}\n" + }, + "v2/interfaces/IAgentJobV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @notice The slice of `AgentJobV2` that other Predge contracts read and drive.\ninterface IAgentJobV2 {\n /// @dev Forward-only, except that `Open` and `Submitted` may each end in `Expired`.\n enum State {\n None,\n Open,\n Submitted,\n Completed,\n Rejected,\n Expired\n }\n\n struct Job {\n address client; // pays; refunded on reject / expiry\n address provider; // does the work; paid on complete\n address evaluator; // the only address that can complete or reject\n uint256 escrow; // native value held for this job until it ends\n uint64 expiredAt; // the provider must submit strictly before this\n uint64 submittedAt; // 0 until submitted\n State state;\n bool hashedOnChain; // true if the deliverable hash was computed on-chain from submitted bytes\n bytes32 specHash; // the committed acceptance test (bond.specHashFor(commitment) for bonded jobs)\n bytes32 deliverable; // sha256 of the provider's output (zero until submitted)\n bytes32 reason; // the evaluator's attestation commitment (zero until decided)\n }\n\n function getJob(uint256 jobId) external view returns (Job memory);\n function complete(uint256 jobId, bytes32 reason, bytes calldata optParams) external;\n function reject(uint256 jobId, bytes32 reason, bytes calldata optParams) external;\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/deployments/v2/verify/PredgeValidatorBondV2.v2-0.json b/deployments/v2/verify/PredgeValidatorBondV2.v2-0.json new file mode 100644 index 0000000..87704f3 --- /dev/null +++ b/deployments/v2/verify/PredgeValidatorBondV2.v2-0.json @@ -0,0 +1,35 @@ +{ + "language": "Solidity", + "sources": { + "v2/PredgeValidatorBondV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Operated} from \"./access/PredgeAccess.sol\";\nimport {PullCredit} from \"./access/PullCredit.sol\";\nimport {IAgentJobV2} from \"./interfaces/IAgentJobV2.sol\";\n\n/// @title PredgeValidatorBondV2\n/// @notice Capital behind a verdict, and the EVALUATOR of an `AgentJobV2`. The job names this\n/// contract as its evaluator, so the verdict recorded here is the thing that moves the\n/// escrow, and the same transaction puts the operator's bond at risk.\n///\n/// The acceptance test is deterministic: the 32-byte deliverable hash stored in the job (sha256 of the\n/// output when the provider submitted the bytes, otherwise the hash the provider claimed) is compared by\n/// RAW EQUALITY with the `expected` 32-byte value the operator committed to. Nothing is hashed again here. That makes a false verdict provable\n/// on-chain with no judge, by comparing two hashes written by two different parties.\n///\n/// Flow:\n/// 1. `stakeAndCommit(requestHash, commitment, jobId)` (operator). Allowed ONLY while\n/// the job is Open, i.e. before any deliverable exists. `commitment` is\n/// `keccak256(abi.encode(expected, salt))`: `expected` stays hidden, so a provider\n/// cannot read it and submit it, and the operator cannot choose it after seeing the\n/// deliverable. The bond must be at least `max(minBond, bondBps * escrow / 10000)`.\n/// 2. The provider submits to the job.\n/// 3. `recordScore(requestHash, score, expected, salt, reason)` (operator). Requires the\n/// job to be Submitted and the reveal to match the commitment. It then calls\n/// `complete` (score 100) or `reject` (score 0) on the job, so the verdict and the\n/// money move together. The dispute window starts at `scoredAt`, which is always at\n/// or after the job's `submittedAt` because scoring requires a submission.\n/// 4. `challenge(requestHash)` (anyone) strictly BEFORE `scoredAt + window`; from that instant\n/// `reclaim` is possible and `challenge` reverts (no overlap, and no open-ended challenge). Slashes iff the\n/// verdict contradicts the deliverable: 100 but deliverable != expected, or 0 but\n/// deliverable == expected. The whole bond is CREDITED TO THE HARMED PARTY (the\n/// job's client on a false pass, its provider on a false fail), never to the caller,\n/// so a self-challenge pays the operator nothing and a front-runner earns nothing.\n/// 5. `reclaim(requestHash)` (anyone) once the window has closed unchallenged. The bond is\n/// credited to the staker recorded at stake time.\n/// 6. `release(requestHash)` (anyone) if the job expired with no verdict. No verdict was\n/// given, so nothing was lied about, and the bond goes back to the staker.\n///\n/// Honest limits, stated so they are not mistaken for guarantees:\n/// * The bond only covers `bondBps` of the escrow. At `bondBps = 10000` a colluding\n/// operator and provider cannot profit from a false pass; below that they can,\n/// up to the uncovered fraction.\n/// * A false verdict is only slashable if the harmed party (or someone for them) calls\n/// `challenge` inside the window. Nobody is paid to watch except the harmed party.\n/// * The operator's capital is locked until the job ends. A client picks the job's\n/// `expiredAt`, so an operator should read it before staking.\n/// * Only the deterministic `sha256(deliverable) == expected` test is covered. Subjective\n/// or \"void\" outcomes are not bonded.\n///\n/// @dev Native value only, in the chain's native wei. Roles: `owner` (Safe) sets bounded\n/// parameters and rotates `operator`; `operator` (hot key) stakes and scores. Parameters\n/// are not retroactive: the dispute window is snapshotted into each stake and the bond\n/// amount is fixed when staked. Payouts are pull-based. The contract holds, at all times,\n/// at least `totalBonded + totalCredit`.\ncontract PredgeValidatorBondV2 is Operated, PullCredit {\n enum Status {\n None,\n Committed, // bond posted, no verdict yet\n Scored, // verdict recorded, window running or elapsed, bond still held\n Slashed, // bond credited to the harmed party (final)\n Reclaimed, // bond credited back to the staker after the window (final)\n Released // job expired with no verdict, bond credited back to the staker (final)\n }\n\n struct Stake {\n address staker; // the operator key that posted the bond; the only party it is ever paid back to\n Status status;\n uint8 score; // 100 delivered / 0 failed, set when scored\n uint64 stakedAt;\n uint64 scoredAt; // set when scored; always >= the job's submittedAt\n uint64 window; // snapshot of `disputeWindow` at stake time\n uint256 jobId;\n uint256 bond;\n bytes32 commitment; // keccak256(abi.encode(expected, salt))\n bytes32 expected; // zero until revealed at recordScore\n }\n\n uint64 public constant MIN_WINDOW = 1 hours;\n uint64 public constant MAX_WINDOW = 30 days;\n uint16 public constant MAX_BPS = 10_000;\n\n IAgentJobV2 public immutable job;\n\n /// @notice Seconds a verdict stays challengeable. Applies to stakes made after a change.\n uint64 public disputeWindow;\n /// @notice Absolute floor for a bond, in native wei. Applies to stakes made after a change.\n uint256 public minBond;\n /// @notice Bond as basis points of the job escrow (10000 = 100%). Applies to future stakes.\n uint16 public bondBps;\n\n /// @notice Sum of bonds currently held (Committed or Scored).\n uint256 public totalBonded;\n uint64 public slashCount;\n\n mapping(bytes32 => Stake) public stakes;\n\n event Committed(bytes32 indexed requestHash, uint256 indexed jobId, address indexed staker, bytes32 commitment, uint256 bond, uint64 window);\n event Scored(bytes32 indexed requestHash, uint256 indexed jobId, uint8 score, bytes32 expected, uint64 scoredAt, uint64 windowEnd);\n event Slashed(bytes32 indexed requestHash, address indexed challenger, address indexed harmed, uint256 bond, uint8 recordedScore, bytes32 delivered);\n event Reclaimed(bytes32 indexed requestHash, address indexed staker, uint256 bond);\n event Released(bytes32 indexed requestHash, address indexed staker, uint256 bond);\n event ParamsUpdated(uint64 disputeWindow, uint256 minBond, uint16 bondBps);\n\n error ZeroHash();\n error BadParams();\n error AlreadyCommitted();\n error NotCommitted();\n error NotScored();\n error BadStatus();\n error NotOpen();\n error NotSubmitted();\n error JobNotBound();\n error ProviderIsOperator();\n error BondTooSmall(uint256 required);\n error BadScore();\n error BadReveal();\n error VerdictHonest();\n error WindowOpen();\n error WindowClosed();\n error JobNotExpired();\n\n /// @param owner_ the Safe: sets bounded parameters, rotates the operator\n /// @param operator_ the hot key that stakes and scores\n /// @param job_ the `AgentJobV2` this bond evaluates; immutable\n /// @param disputeWindow_ seconds a verdict stays challengeable, within [MIN_WINDOW, MAX_WINDOW]\n /// @param minBond_ absolute floor for a bond, non-zero\n /// @param bondBps_ bond as basis points of escrow, at most 10000\n constructor(address owner_, address operator_, address job_, uint64 disputeWindow_, uint256 minBond_, uint16 bondBps_)\n Operated(owner_, operator_)\n {\n if (job_ == address(0)) revert ZeroAddress();\n job = IAgentJobV2(job_);\n _setParams(disputeWindow_, minBond_, bondBps_);\n }\n\n /// @notice Owner-only parameter change, bounded and NOT retroactive: existing stakes keep\n /// the window and bond they were made with.\n function setParams(uint64 disputeWindow_, uint256 minBond_, uint16 bondBps_) external onlyOwner {\n _setParams(disputeWindow_, minBond_, bondBps_);\n }\n\n /// @notice The bond a stake against `escrow` must post right now: `max(minBond, ceil(escrow * bondBps / 10000))`.\n function requiredBond(uint256 escrow) public view returns (uint256) {\n uint256 scaled = (escrow * bondBps + MAX_BPS - 1) / MAX_BPS;\n return scaled > minBond ? scaled : minBond;\n }\n\n /// @notice Post the bond and commit the hidden acceptance test, while the job is still Open.\n /// @param requestHash the job's `specHash` (the domain-separated Predge request hash)\n /// @param commitment `keccak256(abi.encode(expected, salt))`; use a fresh random 32-byte salt\n /// @param jobId the job this verdict will settle; this contract must be its evaluator\n function stakeAndCommit(bytes32 requestHash, bytes32 commitment, uint256 jobId) external payable onlyOperator {\n if (requestHash == bytes32(0) || commitment == bytes32(0)) revert ZeroHash();\n Stake storage s = stakes[requestHash];\n if (s.status != Status.None) revert AlreadyCommitted();\n\n IAgentJobV2.Job memory j = job.getJob(jobId);\n // Only while Open and unexpired: once a deliverable exists, a commitment could be made to match it.\n if (j.state != IAgentJobV2.State.Open || block.timestamp >= j.expiredAt) revert NotOpen();\n if (j.specHash != requestHash || j.evaluator != address(this)) revert JobNotBound();\n // A provider that is also the operator writes both sides of the comparison.\n if (j.provider == msg.sender) revert ProviderIsOperator();\n uint256 required = requiredBond(j.escrow);\n if (msg.value < required) revert BondTooSmall(required);\n\n s.staker = msg.sender;\n s.status = Status.Committed;\n s.stakedAt = uint64(block.timestamp);\n s.window = disputeWindow;\n s.jobId = jobId;\n s.bond = msg.value;\n s.commitment = commitment;\n totalBonded += msg.value;\n emit Committed(requestHash, jobId, msg.sender, commitment, msg.value, s.window);\n }\n\n /// @notice Reveal the committed test, record the verdict, and settle the job with it.\n /// @param score 100 = delivered (job `complete`s, provider is credited), 0 = failed\n /// (job `reject`s, client is credited)\n /// @param expected,salt the preimage of the commitment\n /// @param reason the attestation commitment passed to the job (Predge: keccak256 of the signed verdict)\n function recordScore(bytes32 requestHash, uint8 score, bytes32 expected, bytes32 salt, bytes32 reason)\n external\n onlyOperator\n {\n if (score != 0 && score != 100) revert BadScore();\n Stake storage s = stakes[requestHash];\n if (s.status == Status.None) revert NotCommitted();\n if (s.status != Status.Committed) revert BadStatus();\n if (keccak256(abi.encode(expected, salt)) != s.commitment) revert BadReveal();\n // Re-checked here, not only at stake time: after an operator rotation the NEW operator must not be able\n // to score a job it is the provider of, while inheriting the old operator's stake.\n IAgentJobV2.Job memory j = job.getJob(s.jobId);\n if (j.provider == msg.sender) revert ProviderIsOperator();\n // Scoring needs a submission, so `scoredAt >= submittedAt` and the dispute window can never\n // start before the deliverable it is judged against exists.\n if (j.state != IAgentJobV2.State.Submitted) revert NotSubmitted();\n\n s.expected = expected;\n s.score = score;\n s.scoredAt = uint64(block.timestamp);\n s.status = Status.Scored;\n emit Scored(requestHash, s.jobId, score, expected, s.scoredAt, s.scoredAt + s.window);\n\n if (score == 100) job.complete(s.jobId, reason, \"\");\n else job.reject(s.jobId, reason, \"\");\n }\n\n /// @notice Prove the recorded verdict contradicts the deliverable and slash the bond to the\n /// harmed party. Anyone may call; the caller receives nothing. Takes no evidence.\n function challenge(bytes32 requestHash) external {\n Stake storage s = stakes[requestHash];\n if (s.status != Status.Scored) revert NotScored();\n if (block.timestamp >= uint256(s.scoredAt) + s.window) revert WindowClosed();\n IAgentJobV2.Job memory j = job.getJob(s.jobId);\n if (!_lied(s, j.deliverable)) revert VerdictHonest();\n\n address harmed = s.score == 100 ? j.client : j.provider;\n uint256 bond = s.bond;\n s.status = Status.Slashed;\n s.bond = 0;\n totalBonded -= bond;\n slashCount += 1;\n _addCredit(harmed, bond);\n emit Slashed(requestHash, msg.sender, harmed, bond, s.score, j.deliverable);\n }\n\n /// @notice Credit the bond back to its staker once the dispute window has closed unchallenged.\n /// Anyone may trigger it; the credit always goes to the staker.\n function reclaim(bytes32 requestHash) external {\n Stake storage s = stakes[requestHash];\n if (s.status != Status.Scored) revert NotScored();\n if (block.timestamp < uint256(s.scoredAt) + s.window) revert WindowOpen();\n uint256 bond = _close(s, Status.Reclaimed);\n emit Reclaimed(requestHash, s.staker, bond);\n }\n\n /// @notice Credit the bond back to its staker if the job expired without a verdict.\n function release(bytes32 requestHash) external {\n Stake storage s = stakes[requestHash];\n if (s.status == Status.None) revert NotCommitted();\n if (s.status != Status.Committed) revert BadStatus();\n if (job.getJob(s.jobId).state != IAgentJobV2.State.Expired) revert JobNotExpired();\n uint256 bond = _close(s, Status.Released);\n emit Released(requestHash, s.staker, bond);\n }\n\n /// @notice Is this verdict slashable right now? Free check before spending gas.\n function wouldSlash(bytes32 requestHash) external view returns (bool) {\n Stake storage s = stakes[requestHash];\n if (s.status != Status.Scored || block.timestamp >= uint256(s.scoredAt) + s.window) return false;\n return _lied(s, job.getJob(s.jobId).deliverable);\n }\n\n function _lied(Stake storage s, bytes32 delivered) private view returns (bool) {\n bool matches = delivered == s.expected;\n return (s.score == 100 && !matches) || (s.score == 0 && matches);\n }\n\n function _close(Stake storage s, Status to) private returns (uint256 bond) {\n bond = s.bond;\n s.status = to;\n s.bond = 0;\n totalBonded -= bond;\n _addCredit(s.staker, bond);\n }\n\n function _setParams(uint64 disputeWindow_, uint256 minBond_, uint16 bondBps_) private {\n if (disputeWindow_ < MIN_WINDOW || disputeWindow_ > MAX_WINDOW || minBond_ == 0 || bondBps_ > MAX_BPS)\n revert BadParams();\n disputeWindow = disputeWindow_;\n minBond = minBond_;\n bondBps = bondBps_;\n emit ParamsUpdated(disputeWindow_, minBond_, bondBps_);\n }\n}\n" + }, + "v2/access/PredgeAccess.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title Owned2Step\n/// @notice Minimal two-step ownership, the same hand-over model as OpenZeppelin v5 `Ownable2Step`.\n/// The owner is passed to the constructor (the intended owner is a Safe multisig), so the\n/// deployer key never holds a role. There is deliberately NO `renounceOwnership`: a\n/// renounced owner could never rotate a compromised operator or resolve a dispute.\n/// @dev Two steps: `transferOwnership(next)` nominates, `acceptOwnership()` (called by `next`)\n/// completes. Nominating `address(0)` cancels a pending nomination. A wrong address can\n/// therefore never take ownership, because it has to prove it can sign.\nabstract contract Owned2Step {\n /// @notice Current owner. Config and dispute resolution only; never moves user funds.\n address public owner;\n /// @notice Nominated owner, or zero.\n address public pendingOwner;\n\n event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner);\n event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n\n error NotOwner();\n error NotPendingOwner();\n error ZeroAddress();\n\n modifier onlyOwner() {\n if (msg.sender != owner) revert NotOwner();\n _;\n }\n\n /// @param owner_ initial owner; must not be zero.\n constructor(address owner_) {\n if (owner_ == address(0)) revert ZeroAddress();\n owner = owner_;\n emit OwnershipTransferred(address(0), owner_);\n }\n\n /// @notice Nominate `newOwner`. Passing `address(0)` cancels a pending nomination.\n function transferOwnership(address newOwner) external onlyOwner {\n pendingOwner = newOwner;\n emit OwnershipTransferStarted(owner, newOwner);\n }\n\n /// @notice Complete the hand-over. Only the nominated address can call this.\n function acceptOwnership() external {\n if (msg.sender != pendingOwner) revert NotPendingOwner();\n emit OwnershipTransferred(owner, msg.sender);\n owner = msg.sender;\n pendingOwner = address(0);\n }\n}\n\n/// @title Operated\n/// @notice Adds the hot \"operator\" role (the day-to-day validator / arbiter key) under an\n/// `Owned2Step` owner. The owner can rotate the operator; the operator can never touch\n/// ownership or configuration.\n/// @dev Rotation is not retroactive by design: the contracts that pay out snapshot the staker\n/// address on each stake or ruling, so rotating this key never lets a new key take an old\n/// key's funds, and never strands them either.\nabstract contract Operated is Owned2Step {\n /// @notice The hot operations key. Rotatable by the owner.\n address public operator;\n\n event OperatorUpdated(address indexed previousOperator, address indexed newOperator);\n\n error NotOperator();\n\n modifier onlyOperator() {\n if (msg.sender != operator) revert NotOperator();\n _;\n }\n\n /// @param owner_ initial owner (Safe); must not be zero.\n /// @param operator_ initial operator; must not be zero.\n constructor(address owner_, address operator_) Owned2Step(owner_) {\n if (operator_ == address(0)) revert ZeroAddress();\n operator = operator_;\n emit OperatorUpdated(address(0), operator_);\n }\n\n /// @notice Rotate the operator key. Owner only; the new key must be non-zero.\n function setOperator(address newOperator) external onlyOwner {\n if (newOperator == address(0)) revert ZeroAddress();\n emit OperatorUpdated(operator, newOperator);\n operator = newOperator;\n }\n}\n" + }, + "v2/access/PullCredit.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @title PullCredit\n/// @notice Pull-payment ledger shared by the Predge v2 contracts. Whoever is owed value is\n/// credited here and withdraws it themselves, so a recipient that reverts or is\n/// blocklisted can never force an outcome or lock anybody else's funds.\n/// @dev `totalCredit` is the sum of all outstanding credits; the inheriting contract's native\n/// balance is always at least `totalCredit` plus whatever it still escrows or bonds.\n/// Checks-effects-interactions: the credit is zeroed and the event emitted before the call.\nabstract contract PullCredit {\n /// @notice Withdrawable balance per account.\n mapping(address => uint256) public credit;\n /// @notice Sum of all credits not yet withdrawn.\n uint256 public totalCredit;\n\n event Withdrawn(address indexed account, address indexed to, uint256 amount);\n\n error NothingToWithdraw();\n error TransferFailed();\n error ZeroRecipient();\n\n /// @notice Withdraw the caller's whole credit to the caller.\n function withdraw() external {\n _withdraw(payable(msg.sender));\n }\n\n /// @notice Withdraw the caller's whole credit to `to`, e.g. when the caller's own address\n /// cannot receive value (blocklisted or a contract without a receive function).\n function withdrawTo(address payable to) external {\n if (to == address(0)) revert ZeroRecipient();\n _withdraw(to);\n }\n\n function _addCredit(address to, uint256 amount) internal {\n credit[to] += amount;\n totalCredit += amount;\n }\n\n function _withdraw(address payable to) private {\n uint256 amt = credit[msg.sender];\n if (amt == 0) revert NothingToWithdraw();\n credit[msg.sender] = 0;\n totalCredit -= amt;\n emit Withdrawn(msg.sender, to, amt);\n (bool ok,) = to.call{value: amt}(\"\");\n if (!ok) revert TransferFailed();\n }\n}\n" + }, + "v2/interfaces/IAgentJobV2.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @notice The slice of `AgentJobV2` that other Predge contracts read and drive.\ninterface IAgentJobV2 {\n /// @dev Forward-only, except that `Open` and `Submitted` may each end in `Expired`.\n enum State {\n None,\n Open,\n Submitted,\n Completed,\n Rejected,\n Expired\n }\n\n struct Job {\n address client; // pays; refunded on reject / expiry\n address provider; // does the work; paid on complete\n address evaluator; // the only address that can complete or reject\n uint256 escrow; // native value held for this job until it ends\n uint64 expiredAt; // the provider must submit strictly before this\n uint64 submittedAt; // 0 until submitted\n State state;\n bool hashedOnChain; // true if the deliverable hash was computed on-chain from submitted bytes\n bytes32 specHash; // the committed acceptance test (the Predge requestHash)\n bytes32 deliverable; // sha256 of the provider's output (zero until submitted)\n bytes32 reason; // the evaluator's attestation commitment (zero until decided)\n }\n\n function getJob(uint256 jobId) external view returns (Job memory);\n function complete(uint256 jobId, bytes32 reason, bytes calldata optParams) external;\n function reject(uint256 jobId, bytes32 reason, bytes calldata optParams) external;\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "evmVersion": "cancun", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode.object", + "evm.deployedBytecode.object", + "evm.deployedBytecode.immutableReferences", + "metadata" + ] + } + } + } +} diff --git a/foundry-lib/forge-std b/foundry-lib/forge-std new file mode 160000 index 0000000..8e40513 --- /dev/null +++ b/foundry-lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8e40513d678f392f398620b3ef2b418648b33e89 diff --git a/foundry-v2.toml b/foundry-v2.toml new file mode 100644 index 0000000..4bd763b --- /dev/null +++ b/foundry-v2.toml @@ -0,0 +1,21 @@ +# Foundry config for the Predge v2 contracts (contracts/v2). Kept separate from any v1 foundry.toml so the +# two suites never collide. Use: FOUNDRY_CONFIG=foundry-v2.toml forge test (or `npm run v2:test`) +# Compiler settings mirror lib/compile.mjs exactly: solc 0.8.26, optimizer 200, cancun. +[profile.default] +src = "contracts/v2" +test = "test/foundry-v2" +out = "build-v2/out" +cache_path = "build-v2/cache" +libs = ["foundry-lib"] +remappings = ["forge-std/=foundry-lib/forge-std/src/"] +solc_version = "0.8.26" +optimizer = true +optimizer_runs = 200 +evm_version = "cancun" +bytecode_hash = "ipfs" +fuzz = { runs = 1000 } +invariant = { runs = 256, depth = 64, fail_on_revert = false } + +[profile.ci] +fuzz = { runs = 5000 } +invariant = { runs = 512, depth = 100, fail_on_revert = false } diff --git a/lib/chains.mjs b/lib/chains.mjs index d088696..31778ea 100644 --- a/lib/chains.mjs +++ b/lib/chains.mjs @@ -3,6 +3,7 @@ import { JsonRpcProvider, Network } from "ethers"; export const CHAINS = JSON.parse(readFileSync(new URL("../config/chains.json", import.meta.url), "utf8")); export const SET = JSON.parse(readFileSync(new URL("../config/set.json", import.meta.url), "utf8")); +export const SET_V2 = JSON.parse(readFileSync(new URL("../config/set.v2.json", import.meta.url), "utf8")); export function chainConfig(name) { const c = CHAINS[name]; diff --git a/lib/compile.mjs b/lib/compile.mjs index 8fd9b72..33b93d3 100644 --- a/lib/compile.mjs +++ b/lib/compile.mjs @@ -2,6 +2,7 @@ // Arc / Arbitrum / Robinhood / Monad deployments were built with, so the creation code we ship is // byte-identical to what is already verified there (see `npm test`, which checks it against Arc). import { readFileSync } from "node:fs"; +import { posix } from "node:path"; import { createHash } from "node:crypto"; import solc from "solc"; @@ -15,32 +16,49 @@ export const SETTINGS = { const CONTRACTS_DIR = new URL("../contracts/", import.meta.url); const cache = new Map(); -/** Standard-JSON input for one contract, exactly as compiled (also what explorers verify against). */ -export function standardInput(name) { +/** Pulls in every file `entry` imports (relative imports only), so explorers can verify the multi-file v2 contracts. */ +function collectSources(entry, out = {}) { + if (out[entry]) return out; + const content = readFileSync(new URL(entry, CONTRACTS_DIR), "utf8"); + out[entry] = { content }; + for (const m of content.matchAll(/^\s*import\s+(?:\{[^}]*\}\s+from\s+)?"([^"]+)";/gm)) { + if (!m[1].startsWith(".")) throw new Error(`${entry}: only relative imports are supported (${m[1]})`); + collectSources(posix.normalize(posix.join(posix.dirname(entry), m[1])), out); + } + return out; +} + +/** Standard-JSON input for one contract, exactly as compiled (also what explorers verify against). + * v1: one file `.sol`. v2: pass `file` (e.g. "v2/AgentJobV2") and every imported file is included. */ +export function standardInput(name, file) { + if (file) return { language: "Solidity", sources: collectSources(`${file}.sol`), settings: SETTINGS }; const content = readFileSync(new URL(`${name}.sol`, CONTRACTS_DIR), "utf8"); return { language: "Solidity", sources: { [`${name}.sol`]: { content } }, settings: SETTINGS }; } -export function compile(name) { - if (cache.has(name)) return cache.get(name); +export function compile(name, file) { + const ck = file ? `${file}:${name}` : name; + if (cache.has(ck)) return cache.get(ck); if (!solc.version().startsWith(SOLC_VERSION)) throw new Error(`solc ${solc.version()} != ${SOLC_VERSION}`); - const input = standardInput(name); + const input = standardInput(name, file); + const key = file ? `${file}.sol` : `${name}.sol`; const out = JSON.parse(solc.compile(JSON.stringify(input))); const fatal = (out.errors || []).filter((e) => e.severity === "error"); if (fatal.length) throw new Error(fatal.map((e) => e.formattedMessage).join("\n")); - const c = out.contracts[`${name}.sol`][name]; + const c = out.contracts[key][name]; const res = { name, + id: `${key}:${name}`, abi: c.abi, bytecode: "0x" + c.evm.bytecode.object, deployedBytecode: "0x" + c.evm.deployedBytecode.object, immutableReferences: c.evm.deployedBytecode.immutableReferences || {}, metadata: c.metadata, - sourceSha256: createHash("sha256").update(input.sources[`${name}.sol`].content).digest("hex"), + sourceSha256: createHash("sha256").update(input.sources[key].content).digest("hex"), compilerVersion: "v" + solc.version().replace(".Emscripten.clang", ""), input, }; - cache.set(name, res); + cache.set(ck, res); return res; } diff --git a/lib/key.mjs b/lib/key.mjs index 3d57439..02787b6 100644 --- a/lib/key.mjs +++ b/lib/key.mjs @@ -5,13 +5,13 @@ import { Wallet } from "ethers"; export const KEY_FILE = process.env.PREDGE_DEPLOYER_FILE || `${homedir()}/.predge-deployer/deployer.json`; /** Loads the deployer. Refuses a key file other users can read. Never logs the key. */ -export function loadDeployer(provider) { +export function loadDeployer(provider, file = KEY_FILE) { let st; - try { st = statSync(KEY_FILE); } catch { - throw new Error(`no deployer key at ${KEY_FILE}. Run: npm run keygen`); + try { st = statSync(file); } catch { + throw new Error(`no deployer key at ${file}. Run: npm run keygen`); } - if (st.mode & 0o077) throw new Error(`${KEY_FILE} is readable by others (mode ${(st.mode & 0o777).toString(8)}). chmod 600 it.`); - const j = JSON.parse(readFileSync(KEY_FILE, "utf8")); + if (st.mode & 0o077) throw new Error(`${file} is readable by others (mode ${(st.mode & 0o777).toString(8)}). chmod 600 it.`); + const j = JSON.parse(readFileSync(file, "utf8")); const w = new Wallet(j.privateKey, provider); if (w.address !== j.address) throw new Error("deployer.json address does not match its key"); return w; diff --git a/lib/plan.mjs b/lib/plan.mjs index 6973280..ea7b0f0 100644 --- a/lib/plan.mjs +++ b/lib/plan.mjs @@ -1,12 +1,14 @@ // Resolves the canonical plan for one deployer: every address is known before anything is sent, -// because contract i is created by the deployer's nonce i (CREATE), on every chain. +// because contract i is created by the deployer's nonce `nonceBase + i` (CREATE), on every chain. `nonceBase` comes from +// the set file (0 if absent): a later version of a set can reuse the same deployer key after the earlier version. import { getCreateAddress, ContractFactory } from "ethers"; -import { SET } from "./chains.mjs"; +import { SET as SET_V1 } from "./chains.mjs"; import { compile } from "./compile.mjs"; -export function resolvePlan(deployer, overrides = {}) { +export function resolvePlan(deployer, overrides = {}, SET = SET_V1) { const params = { ...SET.params, ...overrides }; - const addresses = Object.fromEntries(SET.plan.map((s, i) => [s.name, getCreateAddress({ from: deployer, nonce: i })])); + const base = SET.nonceBase ?? 0; + const addresses = Object.fromEntries(SET.plan.map((s, i) => [s.name, getCreateAddress({ from: deployer, nonce: base + i })])); const ctx = { deployer, ...params, ...addresses }; if (params.operator === "$deployer") ctx.operator = deployer; const resolve = (a) => { @@ -15,8 +17,9 @@ export function resolvePlan(deployer, overrides = {}) { if (v === undefined) throw new Error(`unresolved plan arg ${a}`); return v; }; - return SET.plan.map((step, nonce) => { - const c = compile(step.name); + return SET.plan.map((step, i) => { + const nonce = base + i; + const c = compile(step.name, step.file); const args = step.args.map(resolve); const data = new ContractFactory(c.abi, c.bytecode).interface.encodeDeploy(args); return { ...step, nonce, address: addresses[step.name], args, constructorArgs: data, initcode: c.bytecode + data.slice(2), compiled: c }; diff --git a/lib/smoke-v2.mjs b/lib/smoke-v2.mjs new file mode 100644 index 0000000..aa1a8e1 --- /dev/null +++ b/lib/smoke-v2.mjs @@ -0,0 +1,202 @@ +// v2 smoke checks. `readOnlyV2` runs on a live chain (eth_call only). `e2eV2` runs on a local fork ONLY, with the +// public anvil accounts: the full honest path through every contract, then the v1 attack sequences (H-01, H-02, +// H-03) and the Olympix ones (5.1.1, 5.2.2, false verdicts) which must now revert or be harmless. +import { Contract, keccak256, toUtf8Bytes, sha256, AbiCoder, ZeroAddress, ZeroHash } from "ethers"; +import { maskImmutables } from "./compile.mjs"; +import { waitReceipt } from "./chains.mjs"; + +const coder = AbiCoder.defaultAbiCoder(); +const h = (s) => keccak256(toUtf8Bytes(s)); +const step = (plan, name) => plan.find((s) => s.name === name); +const at = (plan, name, runner) => new Contract(step(plan, name).address, step(plan, name).compiled.abi, runner); + +export async function readOnlyV2(provider, plan, roles) { + const checks = []; + const ok = (name, cond, detail) => { checks.push({ check: name, ok: !!cond, detail: String(detail ?? "") }); if (!cond) throw new Error(`v2 smoke failed: ${name} (${detail})`); }; + + for (const s of plan) { + const code = await provider.getCode(s.address); + const c = s.compiled; + ok(`${s.name} runtime == compiled`, maskImmutables(code, c.immutableReferences) === maskImmutables(c.deployedBytecode, c.immutableReferences), s.address); + } + const reg = at(plan, "PredgeAgentValidatorV2", provider); + const job = at(plan, "AgentJobV2", provider); + const bond = at(plan, "PredgeValidatorBondV2", provider); + const settle = at(plan, "PredgeSettlementV2", provider); + const arb = at(plan, "PredgeRefundArbiterV2", provider); + const mock = at(plan, "MockRefundProtocolV2", provider); + const [bondArgs, arbArgs] = [step(plan, "PredgeValidatorBondV2").args, step(plan, "PredgeRefundArbiterV2").args]; + + for (const [n, c] of [["registry", reg], ["bond", bond], ["settlement", settle], ["arbiter", arb]]) { + ok(`${n}.owner == owner (not the deployer)`, (await c.owner()) === roles.owner && roles.owner !== roles.deployer, await c.owner()); + ok(`${n}.pendingOwner == 0`, (await c.pendingOwner()) === ZeroAddress, ""); + } + for (const [n, c] of [["registry", reg], ["bond", bond], ["arbiter", arb]]) ok(`${n}.operator == operator`, (await c.operator()) === roles.operator, await c.operator()); + ok("no contract has a renounceOwnership", await noSelector(provider, plan, "renounceOwnership()"), ""); + ok("registry.identityRegistry", (await reg.identityRegistry()) === step(plan, "PredgeAgentValidatorV2").args[2], await reg.identityRegistry()); + ok("job.evaluationWindow", (await job.evaluationWindow()) === BigInt(step(plan, "AgentJobV2").args[0]), ""); + ok("job.jobCount == 0", (await job.jobCount()) === 0n, ""); + ok("bond.job == AgentJobV2", (await bond.job()) === job.target, await bond.job()); + ok("bond params", (await bond.minBond()) === BigInt(bondArgs[3]) && (await bond.bondBps()) === BigInt(bondArgs[4]), ""); + ok("bond.verdictFor(no job) == 0", (await bond.verdictFor(0n, h("predge/smoke/none"))) === 0n, ""); + ok("bond has no challenge() (v2.1: false verdicts are refused, not slashed)", (await provider.call({ to: bond.target, data: keccak256(toUtf8Bytes("challenge(bytes32)")).slice(0, 10) + "00".repeat(32) }).then(() => false, () => true)), ""); + ok("arbiter.refundProtocol == mock", (await arb.refundProtocol()) === mock.target, await arb.refundProtocol()); + ok("mock.arbiter == arbiter", (await mock.arbiter()) === arb.target, await mock.arbiter()); + ok("arbiter params", (await arb.challengeWindow()) === BigInt(arbArgs[3]) && (await arb.minBond()) === BigInt(arbArgs[4]) && (await arb.bondBps()) === BigInt(arbArgs[5]), ""); + ok("registry.requestHashFor is domain separated", (await reg.requestHashFor(1n, h("d"))) === keccak256(coder.encode(["uint256", "address", "uint256", "bytes32"], [(await provider.getNetwork()).chainId, reg.target, 1n, h("d")])), ""); + ok("bond.specHashFor is domain separated", (await bond.specHashFor(h("c"))) === keccak256(coder.encode(["uint256", "address", "bytes32"], [(await provider.getNetwork()).chainId, bond.target, h("c")])), ""); + return checks; +} + +async function noSelector(provider, plan, sig) { + const data = keccak256(toUtf8Bytes(sig)).slice(0, 10); + for (const s of plan) { + if (s.name === "MockRefundProtocolV2" || s.name === "AgentJobV2") continue; + const r = await provider.call({ to: s.address, data }).then(() => "returned", () => "reverted"); + if (r !== "reverted") return false; + } + return true; +} + +/** `s` = { owner, op, client, worker, party } connected signers on the fork. */ +export async function e2eV2(provider, plan, s, increaseTime) { + const steps = []; + const send = async (label, p) => { const tx = await p; const r = await waitReceipt(provider, tx.hash); if (r.status !== 1) throw new Error(`e2e ${label} reverted`); steps.push({ step: label, gasUsed: Number(r.gasUsed) }); return r; }; + const attacks = []; + /** An attack step passes only if the call reverts with the named custom error. */ + const mustRevert = async (label, name, fn) => { + try { await fn(); } catch (e) { + const got = e.revert?.name ?? e.shortMessage ?? e.message; + if (got !== name) throw new Error(`attack "${label}" reverted with ${got}, expected ${name}`); + attacks.push({ attack: label, blockedBy: name }); return; + } + throw new Error(`attack "${label}" SUCCEEDED, expected revert ${name}`); + }; + const addr = async (w) => w.getAddress(); + const [owner, op, client, worker, party] = [s.owner, s.op, s.client, s.worker, s.party]; + const reg = at(plan, "PredgeAgentValidatorV2", op), job = at(plan, "AgentJobV2", client), bond = at(plan, "PredgeValidatorBondV2", op); + const arb = at(plan, "PredgeRefundArbiterV2", op), mock = at(plan, "MockRefundProtocolV2", client), settle = at(plan, "PredgeSettlementV2", client); + const now = async () => (await provider.getBlock("latest")).timestamp; + const ESCROW = 10n ** 15n; + const output = Buffer.from("predge v2 fork e2e output"); + const OUT = "0x" + output.toString("hex"); + const E = sha256(output), X = h("not the expected output"); + const commit = (expected, salt) => keccak256(coder.encode(["bytes32", "bytes32"], [expected, salt])); + // The CLIENT fixes the expected output in the job's specHash (Olympix 5.1.1): spec = bond.specHashFor(commit(E, salt)). + const newJob = async (n, escrow = ESCROW) => { + const salt = h(`predge/v2-fork/salt/${n}/${Date.now()}`); + const spec = await bond.specHashFor(commit(E, salt)); + await send(`job.createJob #${n}`, job.createJob(await addr(worker), bond.target, spec, (await now()) + 7 * 86400, { value: escrow })); + return { id: await job.jobCount(), spec, salt }; + }; + const stake = async (j, escrow = ESCROW, label = "bond.stakeAndCommit") => { + const need = await bond.requiredBond(escrow); + return send(label, bond.stakeAndCommit(j.spec, commit(E, j.salt), j.id, { value: need })); + }; + + // ---------------- honest path ---------------- + const digest = h(`predge/v2-fork/1/${Date.now()}`); + await send("registry.validationRequest", reg.validationRequest(op.address, 1n, "ipfs://v2-fork", digest)); + const req1 = await reg.requestHashFor(1n, digest); + const j1 = await newJob(1); + await stake(j1); + await send("job.submit (bytes, sha256 on-chain)", job.connect(worker).submit(j1.id, E, OUT)); + if (!(await job.getJob(j1.id)).hashedOnChain) throw new Error("deliverable was not hashed on-chain"); + await send("bond.recordScore(100) completes the job", bond.recordScore(j1.spec, 100, E, j1.salt, h("verdict"))); + if (Number((await job.getJob(j1.id)).state) !== 3) throw new Error("job not Completed by the bond"); + await send("registry.validationResponse", reg.validationResponse(req1, 100, "ipfs://v2-fork/verdict", h("verdict"), "predge/v2-fork")); + if ((await bond.credit(op.address)) < (await bond.requiredBond(ESCROW))) throw new Error("bond not credited back to the staker with the verdict"); + if ((await job.credit(await addr(worker))) !== ESCROW) throw new Error("provider is not credited the escrow"); + await send("job.withdraw (provider pulls payout)", job.connect(worker).withdraw()); + if ((await job.credit(await addr(worker))) !== 0n) throw new Error("provider credit not cleared"); + await send("settlement.payForRoute", settle.payForRoute(h("/v2/route"), "v2-fork", { value: 1000n })); + await send("settlement.withdraw (owner)", settle.connect(owner).withdraw(await addr(owner))); + + // ---------------- attacks that worked on v1 ---------------- + // H-01: verdict before submission; no way to get the bond back while the job runs. + const j2 = await newJob(2); + await mustRevert("Olympix 5.1.1 operator commits a value the client did not fix", "CommitmentNotBound", async () => bond.stakeAndCommit.staticCall(j2.spec, commit(X, j2.salt), j2.id, { value: ESCROW })); + await stake(j2); + await mustRevert("H-01 recordScore before the provider submits", "NotSubmitted", async () => bond.recordScore.staticCall(j2.spec, 100, E, j2.salt, ZeroHash)); + await mustRevert("H-01 bond back without a verdict", "JobNotExpired", async () => bond.release.staticCall(j2.spec)); + // H-02: commit after the deliverable exists / swap the revealed value / provider copies public data. + const j3 = await newJob(3); + await send("job.submit (j3, wrong output)", job.connect(worker).submit(j3.id, X, "0x")); + await mustRevert("H-02 commit after submission", "NotOpen", async () => bond.stakeAndCommit.staticCall(j3.spec, commit(E, j3.salt), j3.id, { value: ESCROW * 2n })); + const j4 = await newJob(4); + await stake(j4); + const staked = await bond.stakes(j4.spec); + if (staked.expected !== ZeroHash) throw new Error("expected is visible on-chain before the reveal"); + await send("job.submit (j4, provider submits the public commitment)", job.connect(worker).submit(j4.id, staked.commitment, "0x")); + await mustRevert("H-02 pass on a copied public value", "VerdictContradictsEvidence", async () => bond.recordScore.staticCall(j4.spec, 100, E, j4.salt, h("lie"))); + await send("bond.recordScore(0) honest fail", bond.recordScore(j4.spec, 0, E, j4.salt, h("fail"))); + // Option (a): a false verdict cannot be recorded at all, whatever the bond size (Olympix 5.1.1 / 5.1.2). + const j5 = await newJob(5); + await stake(j5); + await send("job.submit (j5, wrong output bytes)", job.connect(worker).submit(j5.id, sha256(Buffer.from("wrong")), "0x" + Buffer.from("wrong").toString("hex"))); + await mustRevert("false pass on wrong on-chain bytes", "VerdictContradictsEvidence", async () => bond.recordScore.staticCall(j5.spec, 100, E, j5.salt, h("lie"))); + await send("bond.recordScore(0) on wrong bytes", bond.recordScore(j5.spec, 0, E, j5.salt, h("fail"))); + const j7 = await newJob(7); + await stake(j7); + await send("job.submit (j7, leaked expected, no bytes)", job.connect(worker).submit(j7.id, E, "0x")); + await mustRevert("Olympix 5.1.1 pass on a leaked hash without bytes", "VerdictContradictsEvidence", async () => bond.recordScore.staticCall(j7.spec, 100, E, j7.salt, h("lie"))); + await send("bond.recordScore(0) j7", bond.recordScore(j7.spec, 0, E, j7.salt, h("fail"))); + const j8 = await newJob(8); + await stake(j8); + await send("job.submit (j8, correct bytes)", job.connect(worker).submit(j8.id, E, OUT)); + await mustRevert("false fail on correct on-chain bytes", "VerdictContradictsEvidence", async () => bond.recordScore.staticCall(j8.spec, 0, E, j8.salt, h("lie"))); + await send("bond.recordScore(100) j8", bond.recordScore(j8.spec, 100, E, j8.salt, h("pass"))); + // Olympix 5.2.2: an operator that stays silent on delivered work forfeits its bond to the provider. + const j6 = await newJob(6); + await stake(j6); + await send("job.submit (j6, bytes, operator stays silent)", job.connect(worker).submit(j6.id, E, OUT)); + + // M-03: expiry. An unsubmitted job can be refunded after expiry; the bond is released with no verdict. + await increaseTime(7 * 86400 + 1); + await mustRevert("late submit after expiry", "PastDeadline", async () => job.connect(worker).submit.staticCall(j2.id, E, "0x")); + await send("job.claimRefund (expired, never submitted)", job.connect(party).claimRefund(j2.id)); + await send("bond.release (no verdict was given)", bond.connect(party).release(j2.spec)); + await send("job.claimRefund (j6, silent evaluator)", job.connect(party).claimRefund(j6.id)); + const workerBondBefore = await bond.credit(await addr(worker)); + await send("bond.release (j6, forfeited to the provider)", bond.connect(party).release(j6.spec)); + if ((await bond.credit(await addr(worker))) - workerBondBefore !== (await bond.requiredBond(ESCROW))) throw new Error("silent operator's bond not forfeited to the provider"); + attacks.push({ attack: "Olympix 5.2.2 silence on delivered work is free", blockedBy: "bond forfeited to provider" }); + await send("bond.withdraw (provider gets the forfeited bond)", bond.connect(worker).withdraw()); + + // every scored stake already credited the bond back to the staker + await send("bond.withdraw (staker)", bond.withdraw()); + if ((await bond.totalBonded()) !== 0n) throw new Error("bond still holds stakes after every job ended"); + + // ---------------- arbiter ---------------- + const payer = await addr(party), payee = await addr(worker); + const mkPayment = async (amount) => { + await send("mock.recordPayment", mock.recordPayment(payer, payee, amount, (await now()) + 40 * 86400)); + return mock.paymentCount(); + }; + const p1 = await mkPayment(ESCROW); + const EV = h("payer evidence"), CEV = h("payee counter evidence"); + await mustRevert("H-03 stranger files evidence for someone else's payment", "NotAParty", async () => arb.connect(client).fileEvidence.staticCall(p1, EV)); + await send("arbiter.fileEvidence (payer)", arb.connect(party).fileEvidence(p1, EV)); + await mustRevert("H-03 operator cites a non-party", "NotAParty", async () => arb.rule.staticCall(p1, await addr(client), EV, 1, { value: ESCROW })); + await send("arbiter.rule(Refund)", arb.rule(p1, payer, EV, 1, { value: await arb.requiredBond(ESCROW) })); + if ((await mock.payments(p1)).refunded) throw new Error("refund executed inside rule()"); + attacks.push({ attack: "H-03 refund does not execute inside rule()", blockedBy: "optimistic window" }); + await mustRevert("H-03 finalize inside the window", "WindowOpen", async () => arb.connect(client).finalize.staticCall(p1)); + await send("arbiter.dispute (harmed payee)", arb.connect(worker).dispute(p1, CEV)); + await send("arbiter.resolve(Release) overturns", arb.connect(owner).resolve(p1, 2)); + if ((await mock.payments(p1)).refunded) throw new Error("overturned refund executed"); + if ((await arb.credit(payee)) !== (await arb.requiredBond(ESCROW))) throw new Error("operator bond not paid to the harmed payee"); + await send("arbiter.withdraw (payee gets the operator bond)", arb.connect(worker).withdraw()); + const p2 = await mkPayment(ESCROW); + await send("arbiter.fileEvidence (payer, p2)", arb.connect(party).fileEvidence(p2, EV)); + await send("arbiter.rule(Refund, p2)", arb.rule(p2, payer, EV, 1, { value: await arb.requiredBond(ESCROW) })); + await increaseTime(86401); + await send("arbiter.finalize (undisputed, after window)", arb.connect(client).finalize(p2)); + if (!(await mock.payments(p2)).refunded) throw new Error("undisputed refund did not execute"); + await send("arbiter.withdraw (operator bond back)", arb.withdraw()); + + // ---------------- access ---------------- + await mustRevert("non-owner setOperator", "NotOwner", async () => reg.connect(op).setOperator.staticCall(await addr(client))); + await mustRevert("non-owner transferOwnership", "NotOwner", async () => bond.connect(client).transferOwnership.staticCall(await addr(client))); + return { steps, attacks }; +} diff --git a/lib/v2.mjs b/lib/v2.mjs new file mode 100644 index 0000000..8be0674 --- /dev/null +++ b/lib/v2.mjs @@ -0,0 +1,62 @@ +// v2 deployment inputs: who the roles are, and the per-chain bond floors. +// real runs (dry-run / --broadcast): roles come from config/v2.config.json and MUST be set by the owner. +// --fork: throwaway anvil accounts (the public anvil test keys), never touching ~/.predge-deployer or any real key. +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { getAddress, keccak256, toUtf8Bytes, Wallet } from "ethers"; + +const read = (f) => JSON.parse(readFileSync(new URL(`../config/${f}`, import.meta.url), "utf8")); + +/** Fork-only deployer: derived from a public label, so it is NOT secret and must never hold value anywhere real. The anvil + * default account 0 cannot be used: on mainnets its nonces 0..5 are already taken by other people's contracts (a fork + * deploy at those addresses collides, seen on Arc). */ +export const FORK_DEPLOYER_KEY = keccak256(toUtf8Bytes("predge/v2/fork-deployer")); + +/** The well-known anvil test mnemonic accounts 1..5 as role actors, PUBLIC keys: fork use only, never for a real chain. */ +export const ANVIL_KEYS = [ + FORK_DEPLOYER_KEY, // 0 deployer (fork-only, see above) + "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", // 1 owner (stands in for the Safe) + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", // 2 operator + "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", // 3 client + "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", // 4 provider + "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba", // 5 payer / party +]; +export const anvilWallets = (provider) => ANVIL_KEYS.map((k) => new Wallet(k, provider)); +const ANVIL_ADDRS = new Set(ANVIL_KEYS.map((k) => new Wallet(k).address)); + +const PLACEHOLDER_HELP = "edit config/v2.config.json (deployer = new key created for v2, owner = the Safe, operator = the ops key)"; + +/** Roles for a real run. Throws until the owner has filled them in, and refuses reuse of the v1 deployer. */ +export function realRoles() { + const c = read("v2.config.json"); + const out = {}; + for (const k of ["deployer", "owner", "operator"]) { + if (!c[k]) throw new Error(`v2 config: "${k}" is not set. ${PLACEHOLDER_HELP}`); + out[k] = getAddress(c[k]); + if (ANVIL_ADDRS.has(out[k])) throw new Error(`v2 config: ${k} is a public anvil test address. Use a real one.`); + } + if (new Set(Object.values(out)).size !== 3) throw new Error("v2 config: deployer, owner and operator must be three different addresses"); + const v1 = (() => { try { return JSON.parse(readFileSync(`${homedir()}/.predge-deployer/deployer.json`, "utf8")).address; } catch { return null; } })(); + if (v1 && Object.values(out).includes(getAddress(v1))) throw new Error("v2 config: the v1 deployer address must not hold any v2 role or deploy v2"); + return out; +} + +export function forkRoles() { + const [d, o, p] = ANVIL_KEYS.map((k) => new Wallet(k).address); + return { deployer: d, owner: o, operator: p }; +} + +/** Per-chain overrides for the plan (bond floors). Refuses a chain with no entry rather than guessing. */ +export function chainParams(chainName) { + const c = read("v2.chains.json").chains[chainName]; + if (!c) throw new Error(`config/v2.chains.json has no entry for ${chainName}; run scripts/v2-params.mjs`); + return { minBond: c.minBond, arbiterMinBond: c.arbiterMinBond }; +} + +/** The key file for a real v2 broadcast: must be given explicitly and must not be the v1 deployer's directory. */ +export function v2KeyFile(path) { + if (!path) throw new Error("v2 --broadcast needs --key-file "); + // Block the v1 deployer's directory only (path segment ".predge-deployer"), not ".predge-deployer-v2". + if (path.split(/[\\/]/).includes(".predge-deployer")) throw new Error("refusing to use a key from ~/.predge-deployer for v2"); + return path; +} diff --git a/lib/verify.mjs b/lib/verify.mjs index 74700fd..3938b4e 100644 --- a/lib/verify.mjs +++ b/lib/verify.mjs @@ -11,7 +11,7 @@ async function etherscanLike(base, { chainId, apiKey, step, txHash }) { contractaddress: step.address, sourceCode: JSON.stringify(c.input), codeformat: "solidity-standard-json-input", - contractname: `${step.name}.sol:${step.name}`, + contractname: c.id, compilerversion: c.compilerVersion, constructorArguements: step.constructorArgs.replace(/^0x/, ""), constructorArguments: step.constructorArgs.replace(/^0x/, ""), @@ -39,7 +39,7 @@ async function sourcify({ chainId, step, txHash }) { const c = step.compiled; const r = await fetch(`${SOURCIFY}/v2/verify/${chainId}/${step.address}`, { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ stdJsonInput: c.input, compilerVersion: c.compilerVersion.replace(/^v/, ""), contractIdentifier: `${step.name}.sol:${step.name}`, creationTransactionHash: txHash }), + body: JSON.stringify({ stdJsonInput: c.input, compilerVersion: c.compilerVersion.replace(/^v/, ""), contractIdentifier: c.id, creationTransactionHash: txHash }), }); const j = await r.json().catch(() => ({})); if (r.status === 409) return { ok: true, via: "sourcify", note: "already verified" }; diff --git a/package.json b/package.json index 3f9ae97..f8ef35a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,11 @@ "chains": "node scripts/probe-chains.mjs", "fork-all": "node scripts/fork-all.mjs", "funding": "node scripts/funding.mjs", - "test": "node --test test/" + "test": "node --test test/", + "v2:test": "FOUNDRY_CONFIG=foundry-v2.toml forge test", + "v2:coverage": "FOUNDRY_CONFIG=foundry-v2.toml forge coverage --no-match-coverage \"(test/|foundry-lib|contracts/[A-Za-z]+\\.sol)\"", + "v2:fork": "node deploy.mjs --set v2 --fork --chain", + "v2:mutate": "node scripts/mutate-v2.mjs" }, "engines": { "node": ">=20" }, "dependencies": { diff --git a/scripts/fork-all.mjs b/scripts/fork-all.mjs index 45e9176..a28cd17 100644 --- a/scripts/fork-all.mjs +++ b/scripts/fork-all.mjs @@ -5,21 +5,26 @@ import { writeFileSync, readFileSync, existsSync } from "node:fs"; import { CHAINS } from "../lib/chains.mjs"; const ROOT = new URL("../", import.meta.url).pathname; -const only = process.argv.slice(2); +// usage: node scripts/fork-all.mjs [--set v2] [--no-e2e] [chain ...] +const args = process.argv.slice(2); +const V2 = args.includes("--set") && args[args.indexOf("--set") + 1] === "v2"; +const extra = [...(V2 ? ["--set", "v2"] : []), ...(args.includes("--no-e2e") ? ["--no-e2e"] : [])]; +const DIR = V2 ? "deployments/fork-v2" : "deployments/fork"; +const only = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--set"); const names = only.length ? only : Object.keys(CHAINS); const summary = {}; for (const name of names) { const t = Date.now(); - const r = spawnSync(process.execPath, [`${ROOT}deploy.mjs`, "--chain", name, "--fork"], { encoding: "utf8", timeout: 600000 }); + const r = spawnSync(process.execPath, [`${ROOT}deploy.mjs`, "--chain", name, "--fork", ...extra], { encoding: "utf8", timeout: 600000 }); const out = (r.stdout || "") + (r.stderr || ""); const ok = r.status === 0; - const f = `${ROOT}deployments/fork/${name}.json`; + const f = `${ROOT}${DIR}/${name}.json`; const rec = ok && existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : null; summary[name] = ok ? { ok, seconds: Math.round((Date.now() - t) / 1000), gasUsed: rec.totals.gasUsed, cost: rec.totals.cost, native: rec.totals.native, e2eTxs: rec.e2e?.length ?? 0, smokeChecks: rec.smoke.length } : { ok, error: (out.match(/FAILED: .*/) || [out.slice(-300)])[0] }; console.log(`${name.padEnd(10)} ${ok ? "PASS" : "FAIL"} ${ok ? `${summary[name].gasUsed} gas, ${summary[name].cost} ${summary[name].native}` : summary[name].error}`); } -const file = `${ROOT}deployments/fork/SUMMARY.json`; +const file = `${ROOT}${DIR}/SUMMARY.json`; const prev = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : {}; writeFileSync(file, JSON.stringify({ ...prev, ...summary }, null, 2) + "\n"); diff --git a/scripts/foundry/DeployCircleRefundDemo.s.sol b/scripts/foundry/DeployCircleRefundDemo.s.sol new file mode 100644 index 0000000..07fb425 --- /dev/null +++ b/scripts/foundry/DeployCircleRefundDemo.s.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Script, console2} from "forge-std/Script.sol"; +import {PredgeRefundArbiterV2} from "../../contracts/v2/PredgeRefundArbiterV2.sol"; +import {CircleRefundProtocolAdapter} from "../../contracts/v2/adapters/CircleRefundProtocolAdapter.sol"; +import {MockCircleRefundProtocol} from "../../contracts/v2/mocks/MockCircleRefundProtocol.sol"; + +/// @notice Demo deploy for Arc TESTNET: adapter -> Refund Protocol -> PredgeRefundArbiterV2 -> bind. +/// Simulate first (no --broadcast). Env: +/// OWNER, OPERATOR required; Predge arbiter owner (Safe or EOA) and operator key +/// USDC ERC-20 the Refund Protocol escrows (Arc testnet USDC by default) +/// CIRCLE_RP optional: an existing Circle RefundProtocol whose `arbiter` is the +/// adapter address this script will create. If unset, Predge's own +/// MockCircleRefundProtocol is deployed instead. +/// AMOUNT_SCALE default 1e12 (USDC 6dp -> Arc native 18dp) +/// WINDOW, MIN_BOND, BPS default 86400, 5e18, 0 (the live Arc v2.1 config) +contract DeployCircleRefundDemo is Script { + function run() external { + address owner = vm.envAddress("OWNER"); + address operator = vm.envAddress("OPERATOR"); + address usdc = vm.envOr("USDC", address(0x3600000000000000000000000000000000000000)); + address circleRp = vm.envOr("CIRCLE_RP", address(0)); + uint256 scale = vm.envOr("AMOUNT_SCALE", uint256(1e12)); + uint64 window = uint64(vm.envOr("WINDOW", uint256(86400))); + uint256 minBond = vm.envOr("MIN_BOND", uint256(5e18)); + uint16 bps = uint16(vm.envOr("BPS", uint256(0))); + + vm.startBroadcast(); + CircleRefundProtocolAdapter adapter = new CircleRefundProtocolAdapter(scale); + if (circleRp == address(0)) { + circleRp = address(new MockCircleRefundProtocol(address(adapter), usdc)); + } + PredgeRefundArbiterV2 arb = new PredgeRefundArbiterV2(owner, operator, address(adapter), window, minBond, bps); + adapter.bind(circleRp, address(arb)); + vm.stopBroadcast(); + + console2.log("adapter ", address(adapter)); + console2.log("refundProtocol ", circleRp); + console2.log("predgeArbiter ", address(arb)); + console2.log("next: OWNER calls predgeArbiter.setLockupSeconds(seller, >15 days, <=180 days)"); + } +} diff --git a/scripts/mutate-v2.mjs b/scripts/mutate-v2.mjs new file mode 100644 index 0000000..2621116 --- /dev/null +++ b/scripts/mutate-v2.mjs @@ -0,0 +1,51 @@ +// A small mutation-testing smoke run for contracts/v2, so weak tests are found before an external mutation scanner finds them. +// node scripts/mutate-v2.mjs [--with-invariants] [--only ] [--limit N] +// Each mutant changes ONE line of ONE file, runs the v2 Foundry suite, and is "killed" if any test fails. The original source is +// always restored (and re-checked with git) afterwards. Do not edit contracts/v2 while this runs. +// Operators: == <-> !=, <= < >= > swaps, && <-> ||, true <-> false, += <-> -=, and "disable this guard" (if (c) revert -> if (false && c) revert). +import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { homedir } from "node:os"; + +const ROOT = new URL("../", import.meta.url).pathname; +const args = process.argv.slice(2); +const only = args.includes("--only") ? args[args.indexOf("--only") + 1] : ""; +const limit = args.includes("--limit") ? Number(args[args.indexOf("--limit") + 1]) : Infinity; +const env = { ...process.env, PATH: `${homedir()}/.foundry/bin:${process.env.PATH}`, FOUNDRY_CONFIG: "foundry-v2.toml" }; +const walk = (d) => readdirSync(d).flatMap((n) => { const p = `${d}/${n}`; return statSync(p).isDirectory() ? walk(p) : p.endsWith(".sol") ? [p] : []; }); +const files = walk(`${ROOT}contracts/v2`).filter((f) => !f.includes("/mocks/") && f.includes(only)); + +const swaps = [[/ == /, " != "], [/ != /, " == "], [/ <= /, " < "], [/ < /, " <= "], [/ >= /, " > "], [/ > /, " >= "], [/ && /, " || "], [/ \|\| /, " && "], [/ \+= /, " -= "], [/ -= /, " += "], [/= true;/, "= false;"], [/= false;/, "= true;"]]; +const mutants = []; +for (const f of files) { + const lines = readFileSync(f, "utf8").split("\n"); + lines.forEach((line, i) => { + const t = line.trim(); + if (!t || t.startsWith("//") || t.startsWith("*") || t.startsWith("/*") || t.startsWith("pragma") || t.startsWith("import") || /^(event|error|function|modifier|constructor|struct|enum)\b/.test(t)) return; + if (/=>/.test(line) && !/ == | != |<|>/.test(line.replace(/=>/g, ""))) return; + for (const [re, to] of swaps) { + if (re.test(line)) mutants.push({ f, i, op: `${re.source.trim()} -> ${to.trim()}`, text: line.replace(re, to) }); + } + if (/^if \(.*\) revert /.test(t) || /^if \(.*\)$/.test(t) && lines[i + 1]?.trim().startsWith("revert")) mutants.push({ f, i, op: "guard disabled", text: line.replace("if (", "if (false && ") }); + }); +} +const run = [...mutants].slice(0, limit); +console.log(`${run.length} mutants over ${files.length} files`); +const cmd = ["test", "-q", "--fuzz-runs", "100", ...(args.includes("--with-invariants") ? [] : ["--no-match-path", "test/foundry-v2/Invariants.t.sol"])]; +let killed = 0, invalid = 0; const survivors = []; +for (const m of run) { + const orig = readFileSync(m.f, "utf8"); + const lines = orig.split("\n"); lines[m.i] = m.text; + writeFileSync(m.f, lines.join("\n")); + try { + const r = spawnSync("forge", cmd, { cwd: ROOT, env, encoding: "utf8", timeout: 600000 }); + const out = (r.stdout || "") + (r.stderr || ""); + if (/Compiler run failed|Error: Compiler/.test(out)) invalid++; + else if (r.status !== 0) killed++; + else { survivors.push(m); console.log(`SURVIVED ${m.f.replace(ROOT, "")}:${m.i + 1} [${m.op}] ${m.text.trim()}`); } + } finally { writeFileSync(m.f, orig); } +} +const valid = run.length - invalid; +console.log(`\nkilled ${killed} / ${valid} valid mutants (${invalid} did not compile). score ${(100 * killed / Math.max(1, valid)).toFixed(1)}%. survivors ${survivors.length}`); +const dirty = spawnSync("git", ["diff", "--quiet", "--", "contracts/v2"], { cwd: ROOT }).status; +if (dirty !== 0) { console.error("WARNING: contracts/v2 differs from git after the run"); process.exit(1); } diff --git a/scripts/v2-burn-nonces.mjs b/scripts/v2-burn-nonces.mjs new file mode 100644 index 0000000..74cb38d --- /dev/null +++ b/scripts/v2-burn-nonces.mjs @@ -0,0 +1,60 @@ +#!/usr/bin/env node +// Brings a FRESH v2 deployer up to the set's nonceBase on a new chain, so the v2.1 set lands at the same +// addresses as on Arc and Base (where nonces 0..5 were used by the 20.10 v2.0 set). +// +// node scripts/v2-burn-nonces.mjs --chain dry run: nonce, balance, cost. Sends nothing. +// CONFIRM_MAINNET= node scripts/v2-burn-nonces.mjs --chain --broadcast --key-file ~/.predge-deployer-v2/deployer.json +// +// Each burned nonce is a 0-value transaction from the deployer to itself (no funds leave the address; only gas is paid). +// Refuses: the v1 key, a key that is not the configured v2 deployer, a nonce already past nonceBase, or any code +// already present at a planned v2.1 address. +import { formatEther } from "ethers"; +import { chainConfig, pickRpc, providerFor, withRetry, waitReceipt, SET_V2 } from "../lib/chains.mjs"; +import { loadDeployer } from "../lib/key.mjs"; +import { resolvePlan } from "../lib/plan.mjs"; +import { realRoles, chainParams, v2KeyFile } from "../lib/v2.mjs"; + +const argv = process.argv.slice(2); +const opt = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; }; +const name = opt("--chain"); +if (!name) { console.error("usage: node scripts/v2-burn-nonces.mjs --chain [--broadcast --key-file ]"); process.exit(2); } +const broadcast = argv.includes("--broadcast"); +const chain = chainConfig(name); +const target = SET_V2.nonceBase ?? 0; + +const url = await pickRpc(chain, opt("--rpc")); +const p = providerFor(url, chain.chainId); +const roles = realRoles(); +const plan = resolvePlan(roles.deployer, { owner: roles.owner, operator: roles.operator, ...chainParams(chain.name) }, SET_V2); +for (const s of plan) if ((await p.getCode(s.address)).length > 2) { console.error(`code already at ${s.address} (${s.name}); stop`); process.exit(1); } + +const [bal, nonce, fee] = await Promise.all([p.getBalance(roles.deployer), p.getTransactionCount(roles.deployer, "pending"), p.getFeeData()]); +const price = fee.gasPrice ?? fee.maxFeePerGas; +console.log(`${chain.name}: deployer ${roles.deployer} nonce ${nonce}, target ${target}, balance ${formatEther(bal)} ${chain.native}`); +if (nonce > target) { console.error(`nonce ${nonce} is already past ${target}: same-address deploy impossible here`); process.exit(1); } +const n = target - nonce; +console.log(`${n} self-transaction(s) needed, ~${formatEther(21000n * BigInt(n) * price)} ${chain.native} at ${price} wei`); +if (!broadcast || n === 0) { console.log(broadcast ? "nothing to do" : "dry run only. Nothing sent."); process.exit(0); } +if (process.env.CONFIRM_MAINNET !== chain.name) { console.error(`set CONFIRM_MAINNET=${chain.name}`); process.exit(3); } + +const w = loadDeployer(p, v2KeyFile(opt("--key-file"))); +if (w.address !== roles.deployer) { console.error(`key ${w.address} != config deployer ${roles.deployer}`); process.exit(1); } +for (let i = nonce; i < target; i++) { + // Load-balanced RPCs can lag a block behind our own last receipt: a LOWER nonce is waited out, a HIGHER one is fatal. + let cur = await withRetry("nonce", () => p.getTransactionCount(w.address, "pending")); + for (let k = 0; cur < i && k < 40; k++) { + await new Promise((r) => setTimeout(r, 1500)); + cur = await withRetry("nonce", () => p.getTransactionCount(w.address, "pending")); + } + if (cur !== i) { console.error(`expected nonce ${i}, chain says ${cur}; stop`); process.exit(1); } + const est = await withRetry("estimateGas", () => p.estimateGas({ from: w.address, to: w.address, value: 0n })); + const tx = await w.populateTransaction({ to: w.address, value: 0n, nonce: i, gasLimit: (est * 130n) / 100n }); + const raw = await w.signTransaction(tx); + const sent = await withRetry(`send nonce ${i}`, () => p.broadcastTransaction(raw)); + const r = await waitReceipt(p, sent.hash); + if (r.status !== 1) { console.error(`self-tx at nonce ${i} failed: ${sent.hash}`); process.exit(1); } + console.log(` nonce ${i} burned gas ${r.gasUsed} tx ${sent.hash}`); +} +const after = await p.getTransactionCount(w.address, "latest"); +console.log(`done: nonce ${after}, balance ${formatEther(await p.getBalance(w.address))} ${chain.native}`); +process.exit(after === target ? 0 : 1); diff --git a/scripts/v2-params.mjs b/scripts/v2-params.mjs new file mode 100644 index 0000000..72267e5 --- /dev/null +++ b/scripts/v2-params.mjs @@ -0,0 +1,22 @@ +// Computes per-chain v2 bond floors from live CoinGecko prices and writes config/v2.chains.json. +// node scripts/v2-params.mjs [usdTarget=5] +// Floors are native wei, rounded to ONE significant digit, aiming at `usdTarget` dollars. The point of a floor +// is to stay above what a challenge costs in gas on that chain (v1 used 1e15 everywhere = $0.0002..$4). +// Review the output; the owner can change a floor later with setParams (it only affects future stakes). +import { writeFileSync } from "node:fs"; +import { CHAINS } from "../lib/chains.mjs"; + +const usd = Number(process.argv[2] ?? 5); +const ids = [...new Set(Object.values(CHAINS).map((c) => c.coingecko))]; +const r = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${ids.join(",")}&vs_currencies=usd`, { signal: AbortSignal.timeout(20000) }); +const prices = await r.json(); +const oneSig = (wei) => { const s = wei.toString(); return BigInt(s[0] + "0".repeat(s.length - 1)) ; }; +const out = { _comment: "Generated by scripts/v2-params.mjs. minBond / arbiterMinBond in native wei (18 decimals), ~usdTarget dollars each, 1 significant digit.", generatedAt: new Date().toISOString(), usdTarget: usd, chains: {} }; +for (const [name, c] of Object.entries(CHAINS)) { + const p = prices[c.coingecko]?.usd; + if (!p) throw new Error(`no price for ${c.coingecko}`); + const wei = oneSig(BigInt(Math.round((usd / p) * 1e6)) * 10n ** 12n); + out.chains[name] = { native: c.native, priceUsd: p, minBond: wei.toString(), arbiterMinBond: wei.toString() }; +} +writeFileSync(new URL("../config/v2.chains.json", import.meta.url), JSON.stringify(out, null, 2) + "\n"); +console.log(Object.entries(out.chains).map(([k, v]) => `${k.padEnd(10)} ${v.minBond.padStart(24)} ${v.native} @ $${v.priceUsd}`).join("\n")); diff --git a/scripts/v2-post-deploy.mjs b/scripts/v2-post-deploy.mjs new file mode 100644 index 0000000..e95d247 --- /dev/null +++ b/scripts/v2-post-deploy.mjs @@ -0,0 +1,121 @@ +#!/usr/bin/env node +// After the founder has broadcast the v2.1 set (config/set.v2.json, nonces 6..11) on Arc and Base: +// node scripts/v2-post-deploy.mjs [--chains arc,base] [--from-ref 849d1ff] [--no-verify] [--check] +// +// 1. keeps the 20.10 records: deployments/v2/.v2-0.json and deployments/v2/verify/.v2-0.json, restored +// from git (`--from-ref`, default 849d1ff = the commit that recorded v2.0), because `deploy.mjs --broadcast` has +// overwritten deployments/v2/.json and verify/*.json in place; marks them deprecated; +// 2. links the new record to the old one (`supersedes`); +// 3. retries Sourcify (then Etherscan/Blockscout, as deploy.mjs does) for any contract the broadcast left unverified; +// 4. rewrites the README "v2" deployments section: the v2.1 table as current, the v2.0 addresses marked DEPRECATED. +// Idempotent. Sends no transaction and reads no key. `--check` only validates and prints what it would do. +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { chainConfig, SET_V2 } from "../lib/chains.mjs"; +import { resolvePlan } from "../lib/plan.mjs"; +import { chainParams } from "../lib/v2.mjs"; +import { verifyStep } from "../lib/verify.mjs"; + +const ROOT = new URL("../", import.meta.url).pathname; +const argv = process.argv.slice(2); +const opt = (f, d) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : d; }; +const flag = (f) => argv.includes(f); +const CHAINS = opt("--chains", "arc,base").split(","); +const REF = opt("--from-ref", "849d1ff"); +const OLD_VERSION = "2026-10-20-v2"; +const CHECK = flag("--check"); +const json = (o) => JSON.stringify(o, null, 2) + "\n"; +const git = (...a) => execFileSync("git", ["-C", ROOT, ...a], { encoding: "utf8" }); +const write = (rel, content) => { if (CHECK) console.log(` would write ${rel}`); else writeFileSync(`${ROOT}${rel}`, content); }; +const fail = (m) => { console.error(`v2-post-deploy: ${m}`); process.exit(1); }; + +const records = {}; +for (const c of CHAINS) { + const path = `deployments/v2/${c}.json`; + if (!existsSync(`${ROOT}${path}`)) fail(`${path} missing: broadcast ${c} first`); + const r = JSON.parse(readFileSync(`${ROOT}${path}`, "utf8")); + if (r.setVersion !== SET_V2.version) fail(`${path} has setVersion ${r.setVersion}, expected ${SET_V2.version}: was ${c} broadcast with the v2.1 set?`); + const names = SET_V2.plan.map((s) => s.name); + for (const [i, n] of names.entries()) { + const k = r.contracts[n]; + if (!k) fail(`${path}: ${n} missing`); + if (k.nonce !== (SET_V2.nonceBase ?? 0) + i) fail(`${path}: ${n} at nonce ${k.nonce}, expected ${(SET_V2.nonceBase ?? 0) + i}`); + } + records[c] = r; +} +const addrs = (r) => SET_V2.plan.map((s) => r.contracts[s.name].address); +const first = addrs(records[CHAINS[0]]); +for (const c of CHAINS) if (addrs(records[c]).join() !== first.join()) fail(`addresses differ between ${CHAINS[0]} and ${c}`); + +// 1-2. archive the v2.0 records from git and link them +const old = {}; +for (const c of CHAINS) { + const arch = `deployments/v2/${c}.v2-0.json`; + let o; + if (existsSync(`${ROOT}${arch}`)) o = JSON.parse(readFileSync(`${ROOT}${arch}`, "utf8")); + else { + o = JSON.parse(git("show", `${REF}:deployments/v2/${c}.json`)); + if (o.setVersion !== OLD_VERSION) fail(`${REF}:deployments/v2/${c}.json is ${o.setVersion}, not ${OLD_VERSION}`); + } + o.deprecated = { by: SET_V2.version, record: `${c}.json`, reason: "Olympix BugPoCer scan of c47ed42 (23.10.2026): confirmed findings, fixed in v2.1. Not upgradeable, so the v2.0 instances stay on chain; do not use them." }; + write(arch, json(o)); + old[c] = o; + records[c].supersedes = { setVersion: OLD_VERSION, record: `${c}.v2-0.json`, addresses: Object.fromEntries(Object.entries(o.contracts).map(([k, v]) => [k, v.address])) }; +} +for (const f of git("ls-tree", "--name-only", `${REF}`, "deployments/v2/verify/").split("\n").filter(Boolean)) { + const to = f.replace(/\.json$/, ".v2-0.json"); + if (!existsSync(`${ROOT}${to}`)) write(to, git("show", `${REF}:${f}`)); +} + +// 3. retry verification for anything left unverified +if (!flag("--no-verify")) { + for (const c of CHAINS) { + const r = records[c]; + const chain = chainConfig(c); + const plan = resolvePlan(r.deployer, { owner: r.owner, operator: r.operator, ...chainParams(c) }, SET_V2); + for (const s of plan) { + const k = r.contracts[s.name]; + if (k.address !== s.address) fail(`${c}: ${s.name} recorded at ${k.address}, plan says ${s.address}`); + if (k.verified?.ok) continue; + if (CHECK) { console.log(` would verify ${c} ${s.name}`); continue; } + k.verified = await verifyStep(chain, s, k.txHash); + console.log(` verify ${c} ${s.name}: ${k.verified.ok ? "OK via " + k.verified.via : "TODO " + JSON.stringify(k.verified).slice(0, 200)}`); + } + } +} +for (const c of CHAINS) write(`deployments/v2/${c}.json`, json(records[c])); + +// 4. README +const EXPL = { arc: (a) => `[Arc](https://explorer.arc.io/address/${a})`, base: (a) => `[Base](https://basescan.org/address/${a})` }; +const label = (n) => (n === "MockRefundProtocolV2" ? "MockRefundProtocolV2 (demo refund protocol)" : n); +const row = (n, a) => `| ${label(n)} | \`${a}\` | [Sourcify](https://sourcify.dev/#/lookup/${a}) | ${CHAINS.map((c) => (EXPL[c] ?? (() => c))(a)).join(", ")} |`; +const r0 = records[CHAINS[0]], o0 = old[CHAINS[0]]; +const OWNER_NOTE = (() => { try { return JSON.parse(readFileSync(`${ROOT}config/v2.config.json`, "utf8"))._owner_note ?? ""; } catch { return ""; } })().replace(/^[0-9.]+:\s*/, ""); +const allVerified = CHAINS.every((c) => Object.values(records[c].contracts).every((k) => k.verified?.ok)); +const day = (iso) => iso.slice(0, 10); +const section = `### v2 (current, v2.1): Arc (5042) and Base (8453) + +Deployed ${[...new Set(CHAINS.map((c) => day(records[c].deployedAt)))].join(" / ")} from set \`${SET_V2.version}\` (deployer nonces ${SET_V2.nonceBase}..${SET_V2.nonceBase + SET_V2.plan.length - 1}). The same six addresses on both chains. ${allVerified ? "All six are verified on Sourcify." : "**Verification incomplete: see the `verified` fields in the records.**"} Receipts: ${CHAINS.map((c) => `[\`deployments/v2/${c}.json\`](deployments/v2/${c}.json)`).join(", ")}. + +| Contract | Address (${CHAINS.map((c) => c[0].toUpperCase() + c.slice(1)).join(" and ")}) | Sourcify | Explorer | +|---|---|---|---| +${SET_V2.plan.map((s) => row(s.name, r0.contracts[s.name].address)).join("\n")} + +- **Owner:** \`${r0.owner}\`${OWNER_NOTE ? ` (${OWNER_NOTE})` : ""}. **Operator** (hot ops key, rotatable by the owner): \`${r0.operator}\`. +- **Bond floor:** minimal (bps = 0), per-chain floor from \`config/v2.chains.json\`. Since v2.1 the validator bond backs liveness only: \`recordScore\` refuses any verdict that contradicts the on-chain evidence (pass needs the output bytes hashed on-chain and equal to the client-committed \`expected\`; fail needs the opposite), so a false verdict cannot be recorded at any bond size and there is no challenge window. +- **Status:** v2.1 contains the fixes for the Olympix BugPoCer scan of \`c47ed42\` (23 Oct 2026) and the option (a) verdict check. The signed verdict API (\`api.predge.io\`) is unchanged and remains the product. + +### v2.0 (DEPRECATED, 20-21 Oct 2026): do not use + +The first v2 set (\`${OLD_VERSION}\`, commit \`c47ed42\`) is still on chain at the addresses below, because the contracts are not upgradeable. It is affected by the confirmed Olympix findings and held no funds when it was superseded. Nothing in Predge points at it. Records: ${CHAINS.map((c) => `[\`deployments/v2/${c}.v2-0.json\`](deployments/v2/${c}.v2-0.json)`).join(", ")}. + +| Contract | Deprecated address (${CHAINS.map((c) => c[0].toUpperCase() + c.slice(1)).join(" and ")}) | +|---|---| +${SET_V2.plan.map((s) => `| ${s.name} | ~~\`${o0.contracts[s.name].address}\`~~ |`).join("\n")} + +`; +const readme = readFileSync(`${ROOT}README.md`, "utf8"); +const start = readme.search(/^### v2 \(current/m), end = readme.indexOf("### v1 (legacy)"); +if (start < 0 || end < start) fail("README: could not find the v2 deployments section"); +write("README.md", readme.slice(0, start) + section + readme.slice(end)); +console.log(`${CHECK ? "check ok" : "done"}: ${CHAINS.join(", ")} -> ${first[0]}.. (v2.1); v2.0 archived from ${REF}`); diff --git a/test/foundry-v2/AgentJobV2.t.sol b/test/foundry-v2/AgentJobV2.t.sol new file mode 100644 index 0000000..9bb65a5 --- /dev/null +++ b/test/foundry-v2/AgentJobV2.t.sol @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Base, NoReceive, ReentrantReceiver} from "./Base.sol"; +import {AgentJobV2} from "../../contracts/v2/AgentJobV2.sol"; +import {IAgentJobV2} from "../../contracts/v2/interfaces/IAgentJobV2.sol"; +import {PullCredit} from "../../contracts/v2/access/PullCredit.sol"; + +contract AgentJobV2Test is Base { + address internal evaluator = makeAddr("evaluator"); + bytes32 internal constant SPEC = keccak256("spec"); + bytes32 internal constant DELIV = keccak256("deliverable"); + + event JobCreated(uint256 indexed jobId, address indexed client, address indexed evaluator, address provider, uint256 escrow, uint64 expiredAt, bytes32 specHash); + event Submitted(uint256 indexed jobId, address indexed provider, bytes32 deliverable, bool hashedOnChain); + event Completed(uint256 indexed jobId, address indexed evaluator, address indexed recipient, uint256 amount, bytes32 reason); + event Rejected(uint256 indexed jobId, address indexed evaluator, address indexed recipient, uint256 amount, bytes32 reason); + event Refunded(uint256 indexed jobId, address indexed caller, address indexed recipient, uint256 amount); + event Withdrawn(address indexed account, address indexed to, uint256 amount); + + function setUp() public { + vm.warp(1_760_000_000); + job = new AgentJobV2(EVAL_WINDOW); + vm.deal(client, 100 ether); + } + + function _create(uint256 v) internal returns (uint256 id) { + vm.prank(client); + id = job.createJob{value: v}(provider, evaluator, SPEC, uint64(block.timestamp + 1 days)); + } + + function mk(uint64 w) external returns (address) { + return address(new AgentJobV2(w)); + } + + function test_constructor_rejectsZeroWindow() public { + vm.expectRevert(AgentJobV2.BadWindow.selector); + this.mk(0); + assertEq(job.evaluationWindow(), EVAL_WINDOW); + } + + function test_createJob_storesAndEmits() public { + vm.expectEmit(true, true, true, true); + emit JobCreated(1, client, evaluator, provider, 5 ether, uint64(block.timestamp + 1 days), SPEC); + uint256 id = _create(5 ether); + IAgentJobV2.Job memory j = job.getJob(id); + assertEq(id, 1); + assertEq(j.client, client); + assertEq(j.provider, provider); + assertEq(j.evaluator, evaluator); + assertEq(j.escrow, 5 ether); + assertEq(uint8(j.state), uint8(IAgentJobV2.State.Open)); + assertEq(job.totalEscrowed(), 5 ether); + assertEq(job.jobCount(), 1); + } + + function test_createJob_reverts() public { + vm.startPrank(client); + vm.expectRevert(AgentJobV2.ZeroAddress.selector); + job.createJob{value: 1}(address(0), evaluator, SPEC, uint64(block.timestamp + 1)); + vm.expectRevert(AgentJobV2.ZeroAddress.selector); + job.createJob{value: 1}(provider, address(0), SPEC, uint64(block.timestamp + 1)); + vm.expectRevert(AgentJobV2.ZeroEscrow.selector); + job.createJob(provider, evaluator, SPEC, uint64(block.timestamp + 1)); + vm.expectRevert(AgentJobV2.BadExpiry.selector); + job.createJob{value: 1}(provider, evaluator, SPEC, uint64(block.timestamp)); + vm.stopPrank(); + } + + /// @dev Olympix 5.3.4: neither the client nor the provider can be the evaluator. + function test_createJob_evaluatorNotClientOrProvider() public { + vm.startPrank(client); + vm.expectRevert(AgentJobV2.EvaluatorNotIndependent.selector); + job.createJob{value: 1}(provider, client, SPEC, uint64(block.timestamp + 1)); + vm.expectRevert(AgentJobV2.EvaluatorNotIndependent.selector); + job.createJob{value: 1}(provider, provider, SPEC, uint64(block.timestamp + 1)); + vm.stopPrank(); + } + + function test_submit_hashOnly() public { + uint256 id = _create(1 ether); + vm.expectEmit(true, true, false, true); + emit Submitted(id, provider, DELIV, false); + vm.prank(provider); + job.submit(id, DELIV, ""); + IAgentJobV2.Job memory j = job.getJob(id); + assertEq(j.deliverable, DELIV); + assertFalse(j.hashedOnChain); + assertEq(j.submittedAt, block.timestamp); + assertEq(uint8(j.state), uint8(IAgentJobV2.State.Submitted)); + } + + function test_submit_bytesHashedOnChain() public { + uint256 id = _create(1 ether); + bytes memory out = "the actual output"; + bytes32 h = sha256(out); + vm.prank(provider); + job.submit(id, h, out); + IAgentJobV2.Job memory j = job.getJob(id); + assertEq(j.deliverable, sha256(out)); + assertTrue(j.hashedOnChain); + } + + function test_submit_bytesMismatchReverts() public { + uint256 id = _create(1 ether); + vm.prank(provider); + vm.expectRevert(AgentJobV2.HashMismatch.selector); + job.submit(id, DELIV, "not matching"); + } + + function test_submit_reverts() public { + uint256 id = _create(1 ether); + vm.prank(stranger); + vm.expectRevert(AgentJobV2.NotProvider.selector); + job.submit(id, DELIV, ""); + vm.startPrank(provider); + vm.expectRevert(AgentJobV2.ZeroDeliverable.selector); + job.submit(id, bytes32(0), ""); + vm.expectRevert(AgentJobV2.BadState.selector); + job.submit(99, DELIV, ""); + job.submit(id, DELIV, ""); + vm.expectRevert(AgentJobV2.BadState.selector); + job.submit(id, DELIV, ""); + vm.stopPrank(); + } + + function test_submit_afterExpiryReverts() public { + uint256 id = _create(1 ether); + vm.warp(block.timestamp + 1 days); + vm.prank(provider); + vm.expectRevert(AgentJobV2.PastDeadline.selector); + job.submit(id, DELIV, ""); + } + + function test_complete_creditsProviderPull() public { + uint256 id = _create(2 ether); + vm.prank(provider); + job.submit(id, DELIV, ""); + vm.expectEmit(true, true, true, true); + emit Completed(id, evaluator, provider, 2 ether, bytes32("r")); + vm.prank(evaluator); + job.complete(id, bytes32("r"), ""); + assertEq(job.credit(provider), 2 ether); + assertEq(job.totalCredit(), 2 ether); + assertEq(job.totalEscrowed(), 0); + assertEq(uint8(job.getJob(id).state), uint8(IAgentJobV2.State.Completed)); + assertEq(job.getJob(id).reason, bytes32("r")); + uint256 before_ = provider.balance; + vm.prank(provider); + job.withdraw(); + assertEq(provider.balance, before_ + 2 ether); + assertEq(job.totalCredit(), 0); + } + + function test_reject_creditsClient() public { + uint256 id = _create(2 ether); + vm.prank(provider); + job.submit(id, DELIV, ""); + vm.expectEmit(true, true, true, true); + emit Rejected(id, evaluator, client, 2 ether, bytes32("r")); + vm.prank(evaluator); + job.reject(id, bytes32("r"), ""); + assertEq(job.credit(client), 2 ether); + assertEq(uint8(job.getJob(id).state), uint8(IAgentJobV2.State.Rejected)); + } + + function test_decide_reverts() public { + uint256 id = _create(1 ether); + vm.prank(evaluator); + vm.expectRevert(AgentJobV2.BadState.selector); // not submitted yet + job.complete(id, 0, ""); + vm.prank(provider); + job.submit(id, DELIV, ""); + vm.prank(stranger); + vm.expectRevert(AgentJobV2.NotEvaluator.selector); + job.complete(id, 0, ""); + vm.prank(stranger); + vm.expectRevert(AgentJobV2.NotEvaluator.selector); + job.reject(id, 0, ""); + vm.warp(block.timestamp + EVAL_WINDOW); + vm.prank(evaluator); + vm.expectRevert(AgentJobV2.PastDeadline.selector); + job.complete(id, 0, ""); + } + + function test_decide_onlyOnce() public { + uint256 id = _create(1 ether); + vm.prank(provider); + job.submit(id, DELIV, ""); + vm.startPrank(evaluator); + job.complete(id, 0, ""); + vm.expectRevert(AgentJobV2.BadState.selector); + job.reject(id, 0, ""); + vm.expectRevert(AgentJobV2.BadState.selector); + job.complete(id, 0, ""); + vm.stopPrank(); + } + + // ---- expiry (M-03) ---- + + function test_claimRefund_openJobAfterExpiry() public { + uint256 id = _create(3 ether); + vm.expectRevert(AgentJobV2.NotExpired.selector); + job.claimRefund(id); + vm.warp(block.timestamp + 1 days); + vm.expectEmit(true, true, true, true); + emit Refunded(id, stranger, client, 3 ether); + vm.prank(stranger); // anyone can trigger, the client is credited + job.claimRefund(id); + assertEq(job.credit(client), 3 ether); + assertEq(uint8(job.getJob(id).state), uint8(IAgentJobV2.State.Expired)); + vm.expectRevert(AgentJobV2.BadState.selector); + job.claimRefund(id); + } + + function test_claimRefund_submittedButEvaluatorSilent() public { + uint256 id = _create(3 ether); + vm.prank(provider); + job.submit(id, DELIV, ""); + vm.warp(block.timestamp + EVAL_WINDOW - 1); + vm.expectRevert(AgentJobV2.NotExpired.selector); + job.claimRefund(id); + vm.warp(block.timestamp + 1); + job.claimRefund(id); + assertEq(job.credit(client), 3 ether); + assertEq(job.credit(provider), 0, "documented: silent evaluator refunds the client"); + } + + function test_claimRefund_badStates() public { + vm.expectRevert(AgentJobV2.BadState.selector); + job.claimRefund(42); + uint256 id = _create(1 ether); + vm.prank(provider); + job.submit(id, DELIV, ""); + vm.prank(evaluator); + job.complete(id, 0, ""); + vm.warp(block.timestamp + 365 days); + vm.expectRevert(AgentJobV2.BadState.selector); + job.claimRefund(id); + } + + // ---- pull payments (L-03) ---- + + function test_withdraw_reverts() public { + vm.expectRevert(PullCredit.NothingToWithdraw.selector); + job.withdraw(); + vm.expectRevert(PullCredit.ZeroRecipient.selector); + job.withdrawTo(payable(address(0))); + } + + function test_revertingRecipientCannotBlockOutcomeAndCanRedirect() public { + NoReceive bad = new NoReceive(); + vm.prank(client); + uint256 id = job.createJob{value: 1 ether}(address(bad), evaluator, SPEC, uint64(block.timestamp + 1 days)); + vm.prank(address(bad)); + job.submit(id, DELIV, ""); + vm.prank(evaluator); + job.complete(id, 0, ""); // does not revert even though the provider cannot receive + assertEq(job.credit(address(bad)), 1 ether); + vm.expectRevert(PullCredit.TransferFailed.selector); + bad.callWithdraw(address(job)); + vm.prank(address(bad)); + job.withdrawTo(payable(stranger)); + assertEq(job.credit(address(bad)), 0); + } + + function test_withdraw_isReentrancySafe() public { + ReentrantReceiver r = new ReentrantReceiver(); + r.set(address(job)); + vm.prank(client); + uint256 id = job.createJob{value: 1 ether}(address(r), evaluator, SPEC, uint64(block.timestamp + 1 days)); + vm.prank(address(r)); + job.submit(id, DELIV, ""); + vm.prank(evaluator); + job.complete(id, 0, ""); + r.go(); + assertTrue(r.reentered()); + assertTrue(r.reenterFailed()); + assertEq(address(r).balance, 1 ether); + } + + // ---- fuzz ---- + + function testFuzz_escrowPaidExactlyOnce(uint96 amt, bool pass, bool lateDecision) public { + amt = uint96(bound(amt, 1, 50 ether)); + uint256 id = _create(amt); + vm.prank(provider); + job.submit(id, DELIV, ""); + if (lateDecision) { + vm.warp(block.timestamp + EVAL_WINDOW); + job.claimRefund(id); + assertEq(job.credit(client), amt); + } else { + vm.prank(evaluator); + if (pass) job.complete(id, 0, ""); else job.reject(id, 0, ""); + assertEq(job.credit(pass ? provider : client), amt); + } + assertEq(job.totalEscrowed(), 0); + assertEq(job.totalCredit(), amt); + assertGe(address(job).balance, job.totalCredit()); + } + + function testFuzz_bytesAlwaysHashedOnChain(bytes calldata out) public { + vm.assume(out.length > 0); + uint256 id = _create(1 ether); + bytes32 h = sha256(out); + vm.prank(provider); + job.submit(id, h, out); + assertEq(job.getJob(id).deliverable, h); + } +} diff --git a/test/foundry-v2/Base.sol b/test/foundry-v2/Base.sol new file mode 100644 index 0000000..e7022b9 --- /dev/null +++ b/test/foundry-v2/Base.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {AgentJobV2} from "../../contracts/v2/AgentJobV2.sol"; +import {IAgentJobV2} from "../../contracts/v2/interfaces/IAgentJobV2.sol"; +import {PredgeValidatorBondV2} from "../../contracts/v2/PredgeValidatorBondV2.sol"; +import {PredgeRefundArbiterV2} from "../../contracts/v2/PredgeRefundArbiterV2.sol"; +import {MockRefundProtocolV2} from "../../contracts/v2/mocks/MockRefundProtocolV2.sol"; + +/// @dev A receiver with no receive/fallback: any value sent to it reverts. +contract NoReceive { + function callWithdraw(address target) external { + (bool ok, bytes memory ret) = target.call(abi.encodeWithSignature("withdraw()")); + if (!ok) { + assembly { revert(add(ret, 32), mload(ret)) } + } + } +} + +/// @dev Re-enters `withdraw()` on receipt; used to prove the credit is zeroed before the call. +contract ReentrantReceiver { + address public target; + bool public reentered; + bool public reenterFailed; + + function set(address t) external { target = t; } + function go() external { (bool ok,) = target.call(abi.encodeWithSignature("withdraw()")); require(ok, "outer"); } + receive() external payable { + reentered = true; + (bool ok,) = target.call(abi.encodeWithSignature("withdraw()")); + reenterFailed = !ok; + } +} + +abstract contract Base is Test { + address internal safe = makeAddr("safe"); + address internal op = makeAddr("operator"); + address internal client = makeAddr("client"); + address internal provider = makeAddr("provider"); + address internal stranger = makeAddr("stranger"); + + uint64 internal constant EVAL_WINDOW = 3 days; + uint64 internal constant WINDOW = 1 days; + uint256 internal constant MIN_BOND = 1e15; + uint16 internal constant BPS = 10_000; + + AgentJobV2 internal job; + PredgeValidatorBondV2 internal bond; + + function _deployJobAndBond() internal { + vm.warp(1_760_000_000); // realistic timestamp: block.timestamp == 1 hides arithmetic mutants + job = new AgentJobV2(EVAL_WINDOW); + bond = new PredgeValidatorBondV2(safe, op, address(job), MIN_BOND, BPS); + vm.deal(op, 1_000 ether); + vm.deal(client, 1_000 ether); + vm.deal(provider, 10 ether); + vm.deal(stranger, 10 ether); + } + + function _spec(uint256 n) internal pure returns (bytes32) { + return keccak256(abi.encode("spec", n)); + } + + function _commitment(bytes32 expected, bytes32 salt) internal pure returns (bytes32) { + return keccak256(abi.encode(expected, salt)); + } + + /// @dev The output bytes an honest provider delivers in most tests, and their sha256. + bytes internal constant GOOD = "the correct output"; + + function _good() internal pure returns (bytes32) { + return sha256(GOOD); + } + + /// @dev The specHash a client gives a bonded job: it fixes `expected` (hidden by `salt`). + function _bspec(bytes32 expected, bytes32 salt) internal view returns (bytes32) { + return bond.specHashFor(_commitment(expected, salt)); + } + + /// @dev client opens a job whose evaluator is the bond contract, with a specHash NOT bound to any + /// commitment (no stake can be made on it). For tests that never stake. + function _newJob(uint256 n, uint256 escrow) internal returns (uint256 id, bytes32 spec) { + spec = _spec(n); + vm.prank(client); + id = job.createJob{value: escrow}(provider, address(bond), spec, uint64(block.timestamp + 7 days)); + } + + /// @dev client opens a bonded job and fixes `expected` in its specHash. + function _newJob(uint256 escrow, bytes32 expected, bytes32 salt) internal returns (uint256 id, bytes32 spec) { + spec = _bspec(expected, salt); + vm.prank(client); + id = job.createJob{value: escrow}(provider, address(bond), spec, uint64(block.timestamp + 7 days)); + } + + function _stake(bytes32 spec, bytes32 expected, bytes32 salt, uint256 id, uint256 value) internal { + vm.prank(op); + bond.stakeAndCommit{value: value}(spec, _commitment(expected, salt), id); + } + + /// @dev provider submits a CLAIMED hash only (no bytes): never counts as delivered for the bond. + function _submit(uint256 id, bytes32 deliverable) internal { + vm.prank(provider); + job.submit(id, deliverable, ""); + } + + /// @dev provider submits the output bytes; the job hashes them on-chain. + function _deliver(uint256 id, bytes memory out) internal { + bytes32 h = sha256(out); // computed before the prank: a precompile call would consume it + vm.prank(provider); + job.submit(id, h, out); + } + + function _score(bytes32 spec, uint8 score, bytes32 expected, bytes32 salt) internal { + vm.prank(op); + bond.recordScore(spec, score, expected, salt, keccak256("reason")); + } +} diff --git a/test/foundry-v2/CircleRefundAdapter.t.sol b/test/foundry-v2/CircleRefundAdapter.t.sol new file mode 100644 index 0000000..dc6c776 --- /dev/null +++ b/test/foundry-v2/CircleRefundAdapter.t.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Test, console2} from "forge-std/Test.sol"; +import {PredgeRefundArbiterV2} from "../../contracts/v2/PredgeRefundArbiterV2.sol"; +import {CircleRefundProtocolAdapter} from "../../contracts/v2/adapters/CircleRefundProtocolAdapter.sol"; +import {MockCircleRefundProtocol} from "../../contracts/v2/mocks/MockCircleRefundProtocol.sol"; +import {MockUSDC} from "../../contracts/v2/mocks/MockUSDC.sol"; + +/// @notice PredgeRefundArbiterV2 (unchanged, as deployed) driving a Circle-shaped Refund Protocol +/// through CircleRefundProtocolAdapter. Parameters mirror the live Arc config: 1-day window, +/// 5 USDC native bond floor, 0 bps. +contract CircleRefundAdapterTest is Test { + address internal safe = makeAddr("safe"); + address internal op = makeAddr("operator"); + address internal buyer = makeAddr("buyerAgent"); + address internal seller = makeAddr("sellerAgent"); + address internal stranger = makeAddr("stranger"); + + uint64 internal constant WINDOW = 1 days; + uint256 internal constant MIN_BOND = 5e18; // 5 USDC in Arc native wei + uint16 internal constant BPS = 0; + uint256 internal constant SCALE = 1e12; // USDC 6 decimals -> 18 + uint256 internal constant LOCKUP = 20 days; // > WINDOW + RESOLUTION_TIMEOUT (15 days) + uint256 internal constant PRICE = 10e6; // 10 USDC + + bytes32 internal constant EV_BUYER = keccak256("terms+delivery hash, buyer"); + bytes32 internal constant EV_SELLER = keccak256("terms+delivery hash, seller"); + bytes32 internal constant EV_COUNTER = keccak256("counter evidence"); + + MockUSDC internal usdc; + MockCircleRefundProtocol internal circle; + CircleRefundProtocolAdapter internal adapter; + PredgeRefundArbiterV2 internal arb; + + function setUp() public { + vm.warp(1_762_000_000); + usdc = new MockUSDC(); + adapter = new CircleRefundProtocolAdapter(SCALE); + circle = new MockCircleRefundProtocol(address(adapter), address(usdc)); + arb = new PredgeRefundArbiterV2(safe, op, address(adapter), WINDOW, MIN_BOND, BPS); + adapter.bind(address(circle), address(arb)); + + vm.prank(safe); + arb.setLockupSeconds(seller, LOCKUP); // Safe -> Predge arbiter -> adapter -> Circle + + vm.deal(op, 100 ether); + usdc.mint(buyer, 100e6); + vm.prank(buyer); + usdc.approve(address(circle), type(uint256).max); + } + + // ---------- helpers ---------- + + function _pay() internal returns (uint256 id) { + id = circle.nonce(); + vm.prank(buyer); + circle.pay(seller, PRICE, buyer); + } + + function _fileBoth(uint256 id) internal { + vm.prank(buyer); + arb.fileEvidence(id, EV_BUYER); + vm.prank(seller); + arb.fileEvidence(id, EV_SELLER); + } + + function _rule(uint256 id, address from, bytes32 ev, PredgeRefundArbiterV2.Direction d) internal { + vm.prank(op); + arb.rule{value: MIN_BOND}(id, from, ev, d); + } + + function _refunded(uint256 id) internal view returns (bool r) { + (,,,,, r) = circle.payments(id); + } + + // ---------- wiring ---------- + + function test_wiring() public view { + assertEq(circle.arbiter(), address(adapter)); + assertEq(address(arb.refundProtocol()), address(adapter)); + assertEq(address(adapter.circle()), address(circle)); + assertEq(adapter.predgeArbiter(), address(arb)); + assertEq(circle.lockupSeconds(seller), LOCKUP); + } + + function test_bind_onceOnly_deployerOnly() public { + vm.expectRevert(CircleRefundProtocolAdapter.AlreadyBound.selector); + adapter.bind(address(circle), address(arb)); + + CircleRefundProtocolAdapter a2 = new CircleRefundProtocolAdapter(SCALE); + vm.prank(stranger); + vm.expectRevert(CircleRefundProtocolAdapter.NotDeployer.selector); + a2.bind(address(circle), address(arb)); + // Circle's arbiter is the first adapter, not a2 + vm.expectRevert(CircleRefundProtocolAdapter.NotCircleArbiter.selector); + a2.bind(address(circle), address(arb)); + // Predge arbiter points to the first adapter, not a2 + MockCircleRefundProtocol c2 = new MockCircleRefundProtocol(address(a2), address(usdc)); + vm.expectRevert(CircleRefundProtocolAdapter.ArbiterNotWired.selector); + a2.bind(address(c2), address(arb)); + vm.expectRevert(CircleRefundProtocolAdapter.NotBound.selector); + a2.paymentInfo(0); + } + + function test_paymentInfo_mapsCircleFields() public { + uint256 id = _pay(); + assertEq(id, 0, "Circle ids start at 0"); + (address payer, address payee, uint256 amount, uint256 release) = adapter.paymentInfo(id); + assertEq(payer, buyer, "payer = refundTo"); + assertEq(payee, seller, "payee = to"); + assertEq(amount, PRICE * SCALE, "6 -> 18 decimals"); + assertEq(release, block.timestamp + LOCKUP); + (payer, payee, amount, release) = adapter.paymentInfo(99); + assertEq(payee, address(0), "unknown payment reads as zero"); + } + + function test_onlyPredgeArbiterCanDriveCircle() public { + uint256 id = _pay(); + vm.prank(stranger); + vm.expectRevert(CircleRefundProtocolAdapter.NotPredgeArbiter.selector); + adapter.refundByArbiter(id); + vm.prank(op); + vm.expectRevert(CircleRefundProtocolAdapter.NotPredgeArbiter.selector); + adapter.refundByArbiter(id); + vm.prank(stranger); + vm.expectRevert(MockCircleRefundProtocol.CallerNotAllowed.selector); + circle.refundByArbiter(id); + vm.prank(stranger); + vm.expectRevert(CircleRefundProtocolAdapter.NotPredgeArbiter.selector); + adapter.setLockupSeconds(seller, 1); + } + + // ---------- 1. happy path: release ---------- + + function test_release_sellerPaidAfterLockup() public { + uint256 id = _pay(); + _fileBoth(id); + _rule(id, seller, EV_SELLER, PredgeRefundArbiterV2.Direction.Release); + + vm.warp(block.timestamp + WINDOW); + arb.finalize(id); + assertEq(uint8(arb.rulings(id).status), uint8(PredgeRefundArbiterV2.Status.Finalized)); + assertFalse(_refunded(id)); + assertEq(arb.credit(op), MIN_BOND, "bond back to operator"); + + vm.warp(block.timestamp + LOCKUP); + uint256[] memory ids = new uint256[](1); + ids[0] = id; + vm.prank(seller); + circle.withdraw(ids); + assertEq(usdc.balanceOf(seller), PRICE); + assertEq(usdc.balanceOf(address(circle)), 0); + + uint256 before = op.balance; + vm.prank(op); + arb.withdraw(); + assertEq(op.balance - before, MIN_BOND); + } + + // ---------- 2. refund on verdict ---------- + + function test_refundOnVerdict_buyerGetsUsdcBack() public { + uint256 id = _pay(); + _fileBoth(id); + _rule(id, buyer, EV_BUYER, PredgeRefundArbiterV2.Direction.Refund); + + vm.warp(block.timestamp + WINDOW); + arb.finalize(id); + assertTrue(_refunded(id)); + assertEq(usdc.balanceOf(buyer), 100e6, "buyer whole again"); + assertEq(circle.balances(seller), 0); + + vm.warp(block.timestamp + LOCKUP); + uint256[] memory ids = new uint256[](1); + ids[0] = id; + vm.prank(seller); + vm.expectRevert(abi.encodeWithSelector(MockCircleRefundProtocol.PaymentRefunded.selector, id)); + circle.withdraw(ids); + } + + // ---------- 3. dispute ---------- + + function test_dispute_releaseOverturnedToRefund() public { + uint256 id = _pay(); + _fileBoth(id); + _rule(id, seller, EV_SELLER, PredgeRefundArbiterV2.Direction.Release); + + vm.prank(seller); + vm.expectRevert(PredgeRefundArbiterV2.NotHarmedParty.selector); + arb.dispute(id, EV_COUNTER); + + vm.prank(buyer); + arb.dispute(id, EV_COUNTER); + vm.warp(block.timestamp + WINDOW); + vm.expectRevert(PredgeRefundArbiterV2.BadStatus.selector); + arb.finalize(id); // frozen + + vm.prank(safe); + arb.resolve(id, PredgeRefundArbiterV2.Direction.Refund); + assertEq(uint8(arb.rulings(id).status), uint8(PredgeRefundArbiterV2.Status.Overturned)); + assertTrue(_refunded(id)); + assertEq(usdc.balanceOf(buyer), 100e6); + assertEq(arb.credit(buyer), MIN_BOND, "operator bond to the harmed buyer"); + assertEq(arb.overturnCount(), 1); + } + + function test_dispute_refundUpheld() public { + uint256 id = _pay(); + _fileBoth(id); + _rule(id, buyer, EV_BUYER, PredgeRefundArbiterV2.Direction.Refund); + vm.prank(seller); + arb.dispute(id, EV_COUNTER); + vm.prank(safe); + arb.resolve(id, PredgeRefundArbiterV2.Direction.Refund); + assertEq(uint8(arb.rulings(id).status), uint8(PredgeRefundArbiterV2.Status.Finalized)); + assertTrue(_refunded(id)); + assertEq(arb.credit(op), MIN_BOND); + } + + // ---------- edge cases from the gap analysis ---------- + + function test_sellerRefundsFirst_finalizeStillSucceeds() public { + uint256 id = _pay(); + _fileBoth(id); + _rule(id, buyer, EV_BUYER, PredgeRefundArbiterV2.Direction.Refund); + vm.prank(seller); + circle.refundByRecipient(id); + vm.warp(block.timestamp + WINDOW); + arb.finalize(id); // adapter treats "already refunded" as done + assertEq(uint8(arb.rulings(id).status), uint8(PredgeRefundArbiterV2.Status.Finalized)); + assertEq(usdc.balanceOf(buyer), 100e6); + } + + function test_refundedPayment_notArbitrable() public { + uint256 id = _pay(); + vm.prank(seller); + circle.refundByRecipient(id); + vm.prank(buyer); + vm.expectRevert(PredgeRefundArbiterV2.NotAParty.selector); + arb.fileEvidence(id, EV_BUYER); + } + + function test_zeroLockup_ruleRefused() public { + address other = makeAddr("noLockupSeller"); + uint256 id = circle.nonce(); + vm.prank(buyer); + circle.pay(other, PRICE, buyer); + vm.prank(buyer); + arb.fileEvidence(id, EV_BUYER); + vm.prank(op); + vm.expectRevert(PredgeRefundArbiterV2.LockupTooShort.selector); + arb.rule{value: MIN_BOND}(id, buyer, EV_BUYER, PredgeRefundArbiterV2.Direction.Refund); + } + + function test_lockupCap_circle180dBelowPredge365d() public { + vm.prank(safe); + vm.expectRevert(MockCircleRefundProtocol.LockupSecondsExceedsMax.selector); + arb.setLockupSeconds(seller, 200 days); + } + + function test_refundToChangedAfterRuling_refundGoesToNewAddress() public { + address newTo = makeAddr("buyerTreasury"); + uint256 id = _pay(); + _fileBoth(id); + _rule(id, buyer, EV_BUYER, PredgeRefundArbiterV2.Direction.Refund); + vm.prank(buyer); + circle.updateRefundTo(id, newTo); + vm.warp(block.timestamp + WINDOW); + arb.finalize(id); + assertEq(usdc.balanceOf(newTo), PRICE, "Circle pays the current refundTo"); + assertEq(arb.rulings(id).payer, buyer, "Predge keeps the payer snapshotted at ruling"); + } + + // ---------- the 6-line demo log ---------- + + function test_demoLog() public { + uint256 id = _pay(); + console2.log("[1/6] PAY buyer-agent -> RefundProtocol (Circle interface): 10.00 USDC to seller-agent, id", id, "lockup 20d"); + _fileBoth(id); + console2.log("[2/6] EVIDENCE buyer + seller filed hashes of terms and delivery with Predge"); + _rule(id, buyer, EV_BUYER, PredgeRefundArbiterV2.Direction.Refund); + console2.log("[3/6] VERDICT Predge operator: REFUND (delivery did not meet the fixed terms), 5 USDC bond posted"); + console2.log("[4/6] WINDOW 24h challenge window open; seller may dispute to the owner Safe"); + vm.warp(block.timestamp + WINDOW); + arb.finalize(id); + console2.log("[5/6] FINALIZE no dispute -> Predge arbiter -> adapter -> RefundProtocol.refundByArbiter"); + console2.log("[6/6] RESULT buyer USDC back (6dp):", usdc.balanceOf(buyer) - 90e6, "refunded:", _refunded(id)); + assertTrue(_refunded(id)); + } +} diff --git a/test/foundry-v2/ConstructorGuards.t.sol b/test/foundry-v2/ConstructorGuards.t.sol new file mode 100644 index 0000000..84d0f21 --- /dev/null +++ b/test/foundry-v2/ConstructorGuards.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {AgentJobV2} from "../../contracts/v2/AgentJobV2.sol"; +import {PredgeValidatorBondV2} from "../../contracts/v2/PredgeValidatorBondV2.sol"; +import {PredgeRefundArbiterV2} from "../../contracts/v2/PredgeRefundArbiterV2.sol"; +import {MockRefundProtocolV2} from "../../contracts/v2/mocks/MockRefundProtocolV2.sol"; +import {Owned2Step} from "../../contracts/v2/access/PredgeAccess.sol"; + +/// @notice Constructor guards and constants, tested in named test functions with NO setUp. +/// @dev Olympix mutation run on c47ed42: four constructor-guard mutants (`== address(0)` -> `!=`, +/// `bondBps_ > MAX_BPS` -> `<`, `evaluationWindow_ == 0` -> `!=`) were reported as surviving although +/// they make every deployment revert. In our suites they only broke `setUp()`, which a runner that +/// attributes kills to named test functions does not count. Here every deployment happens inside a +/// test function, so such a mutant fails a named test. +contract ConstructorGuardsTest is Test { + address internal safe = makeAddr("safe"); + address internal op = makeAddr("operator"); + + function mkJob(uint64 w) external returns (address) { + return address(new AgentJobV2(w)); + } + + function mkBond(address j, uint16 bps) external returns (address) { + return address(new PredgeValidatorBondV2(safe, op, j, 1, bps)); + } + + function mkArb(address rp, uint16 bps) external returns (address) { + return address(new PredgeRefundArbiterV2(safe, op, rp, 1 hours, 1, bps)); + } + + function test_job_constructorGuard() public { + vm.expectRevert(AgentJobV2.BadWindow.selector); + this.mkJob(0); + AgentJobV2 j = AgentJobV2(this.mkJob(1)); + assertEq(j.evaluationWindow(), 1, "a non-zero window deploys"); + } + + function test_bond_constructorGuards() public { + address j = this.mkJob(1 days); + vm.expectRevert(Owned2Step.ZeroAddress.selector); + this.mkBond(address(0), 0); + PredgeValidatorBondV2 b = PredgeValidatorBondV2(this.mkBond(j, 0)); + assertEq(address(b.job()), j, "a non-zero job deploys"); + assertEq(PredgeValidatorBondV2(this.mkBond(j, 10_000)).bondBps(), 10_000); + vm.expectRevert(PredgeValidatorBondV2.BadParams.selector); + this.mkBond(j, 10_001); + } + + function test_arbiter_constructorGuards() public { + address rp = address(new MockRefundProtocolV2(address(this))); + vm.expectRevert(Owned2Step.ZeroAddress.selector); + this.mkArb(address(0), 0); + PredgeRefundArbiterV2 a = PredgeRefundArbiterV2(this.mkArb(rp, 0)); + assertEq(address(a.refundProtocol()), rp, "a non-zero refund protocol deploys"); + assertEq(PredgeRefundArbiterV2(this.mkArb(rp, 500)).bondBps(), 500, "bps below the cap deploys"); + assertEq(PredgeRefundArbiterV2(this.mkArb(rp, 10_000)).bondBps(), 10_000, "bps at the cap deploys"); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + this.mkArb(rp, 10_001); + } + + function test_arbiter_constants() public { + address rp = address(new MockRefundProtocolV2(address(this))); + PredgeRefundArbiterV2 a = PredgeRefundArbiterV2(this.mkArb(rp, 0)); + assertEq(a.RESOLUTION_TIMEOUT(), 14 days); + assertEq(a.MIN_WINDOW(), 1 hours); + assertEq(a.MAX_WINDOW(), 30 days); + assertEq(a.MAX_LOCKUP(), 365 days); + } +} diff --git a/test/foundry-v2/Invariants.t.sol b/test/foundry-v2/Invariants.t.sol new file mode 100644 index 0000000..38ea392 --- /dev/null +++ b/test/foundry-v2/Invariants.t.sol @@ -0,0 +1,436 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {StdInvariant} from "forge-std/StdInvariant.sol"; +import {Test} from "forge-std/Test.sol"; +import {AgentJobV2} from "../../contracts/v2/AgentJobV2.sol"; +import {IAgentJobV2} from "../../contracts/v2/interfaces/IAgentJobV2.sol"; +import {PredgeValidatorBondV2} from "../../contracts/v2/PredgeValidatorBondV2.sol"; +import {PredgeRefundArbiterV2} from "../../contracts/v2/PredgeRefundArbiterV2.sol"; +import {MockRefundProtocolV2} from "../../contracts/v2/mocks/MockRefundProtocolV2.sol"; + +/// @dev Drives AgentJobV2 + PredgeValidatorBondV2 through random interleavings and records ghost state. +contract JobBondHandler is Test { + AgentJobV2 public job; + PredgeValidatorBondV2 public bond; + address public op = address(0x0A11); + address public client = address(0xC11E); + address public provider = address(0x9A0D); + address public safe = address(0x5AFE); + + bytes32[] public specs; // request hashes of every job created + uint256[] public jobIds; + mapping(bytes32 => bytes32) public expectedOf; // the value the client fixed in the specHash + mapping(bytes32 => bytes32) public saltOf; + mapping(bytes32 => bytes) public outputOf; // the bytes whose sha256 is expectedOf + mapping(bytes32 => uint8) public prevStatus; // last status seen, to prove monotonicity + mapping(uint256 => uint8) public prevJobState; + mapping(bytes32 => bytes32) public firstCommitment; + mapping(bytes32 => bytes32) public firstExpected; + mapping(bytes32 => uint256) public firstJobId; + mapping(bytes32 => uint64) public firstScoredAt; + mapping(bytes32 => uint8) public firstScore; + + uint256 public ghostCreditedFromJobs; // everything the job ever credited + uint256 public ghostEscrowedTotal; // everything ever escrowed + bool public violation; // set by in-handler checks that need to see the call itself + + constructor(AgentJobV2 j, PredgeValidatorBondV2 b) { + job = j; + bond = b; + vm.deal(op, type(uint128).max); + vm.deal(client, type(uint128).max); + vm.deal(provider, type(uint128).max); + } + + function specCount() external view returns (uint256) { return specs.length; } + + function createAndStake(uint96 escrow, uint8 salt, bool stakeIt) external { + escrow = uint96(bound(escrow, 1, 1_000 ether)); + // The client fixes the expected output in the specHash (Olympix 5.1.1). + bytes memory out = abi.encode("e", salt); + bytes32 e = sha256(out); + bytes32 s = keccak256(abi.encode("salt", salt, specs.length)); + bytes32 spec = bond.specHashFor(keccak256(abi.encode(e, s))); + vm.prank(client); + uint256 id = job.createJob{value: escrow}(provider, address(bond), spec, uint64(block.timestamp + 5 days)); + ghostEscrowedTotal += escrow; + specs.push(spec); + jobIds.push(id); + expectedOf[spec] = e; + saltOf[spec] = s; + outputOf[spec] = out; + if (stakeIt) { + uint256 need = bond.requiredBond(escrow); + vm.prank(op); + try bond.stakeAndCommit{value: need}(spec, keccak256(abi.encode(e, s)), id) {} catch {} + } + } + + /// @dev mode 0: the real bytes; 1: the right hash CLAIMED with no bytes; 2: junk bytes; 3: a junk claimed hash. + function submit(uint256 i, uint8 mode, uint8 junk) external { + if (specs.length == 0) return; + i = bound(i, 0, specs.length - 1); + bytes32 spec = specs[i]; + mode = mode % 4; + bytes memory out = mode == 0 ? outputOf[spec] : mode == 2 ? abi.encode("junk", junk) : bytes(""); + bytes32 d = mode == 1 ? expectedOf[spec] : mode == 3 ? keccak256(abi.encode("junk", junk)) : sha256(out); + vm.prank(provider); + try job.submit(jobIds[i], d, out) {} catch {} + } + + /// @dev Option (a): a recorded verdict must equal the truth, so a successful call with a false score is a violation. + function score(uint256 i, bool pass) external { + if (specs.length == 0) return; + i = bound(i, 0, specs.length - 1); + bytes32 spec = specs[i]; + IAgentJobV2.Job memory j = job.getJob(jobIds[i]); + bool delivered = j.hashedOnChain && j.deliverable == expectedOf[spec]; + vm.prank(bond.operator()); + try bond.recordScore(spec, pass ? 100 : 0, expectedOf[spec], saltOf[spec], bytes32(0)) { + if (pass != delivered) violation = true; + } catch {} + } + + function release(uint256 i) external { + if (specs.length == 0) return; + i = bound(i, 0, specs.length - 1); + try bond.release(specs[i]) {} catch {} + } + + function claimRefund(uint256 i) external { + if (specs.length == 0) return; + i = bound(i, 0, specs.length - 1); + try job.claimRefund(jobIds[i]) {} catch {} + } + + function withdrawAll(uint8 who) external { + address a = who % 3 == 0 ? client : who % 3 == 1 ? provider : op; + vm.prank(a); + try job.withdraw() {} catch {} + vm.prank(a); + try bond.withdraw() {} catch {} + } + + function rotateOperator(bool back) external { + vm.prank(safe); + bond.setOperator(back ? op : address(0xBEEF)); + vm.deal(address(0xBEEF), 1); + } + + function warp(uint32 dt) external { + vm.warp(block.timestamp + bound(dt, 0, 4 days)); + _record(); + } + + /// @dev Snapshot ghost values and check write-once / monotonic properties. + function _record() internal { + for (uint256 i; i < specs.length; ++i) { + _checkStake(specs[i]); + IAgentJobV2.Job memory j = job.getJob(jobIds[i]); + if (uint8(j.state) < prevJobState[jobIds[i]]) violation = true; + prevJobState[jobIds[i]] = uint8(j.state); + } + } + + function _checkStake(bytes32 spec) internal { + (, PredgeValidatorBondV2.Status st, uint8 sc, , uint64 scoredAt, uint256 jid, , bytes32 c, bytes32 e) = bond.stakes(spec); + uint8 s8 = uint8(st); + if (s8 < prevStatus[spec]) violation = true; // never moves backwards + if (prevStatus[spec] >= 2 && s8 != prevStatus[spec]) violation = true; // Scored / Released / Forfeited are final + if (s8 != 0) { + if (firstCommitment[spec] == 0) { firstCommitment[spec] = c; firstJobId[spec] = jid; } + if (c != firstCommitment[spec] || jid != firstJobId[spec]) violation = true; + } + if (s8 == 2) { // only Scored carries a verdict + if (firstScoredAt[spec] == 0) { firstScoredAt[spec] = scoredAt; firstExpected[spec] = e; firstScore[spec] = sc; } + if (scoredAt != firstScoredAt[spec] || e != firstExpected[spec] || sc != firstScore[spec]) violation = true; + } + prevStatus[spec] = s8; + } + + function snapshot() external { _record(); } +} + +contract JobBondInvariantTest is StdInvariant, Test { + AgentJobV2 job; + PredgeValidatorBondV2 bond; + JobBondHandler h; + + function setUp() public { + job = new AgentJobV2(3 days); + bond = new PredgeValidatorBondV2(address(0x5AFE), address(0x0A11), address(job), 1e15, 10_000); + h = new JobBondHandler(job, bond); + targetContract(address(h)); + } + + function invariant_bondSolvent() public view { + assertGe(address(bond).balance, bond.totalBonded() + bond.totalCredit()); + } + + function invariant_jobSolvent() public view { + assertGe(address(job).balance, job.totalEscrowed() + job.totalCredit()); + } + + function invariant_totalBondedEqualsOpenBonds() public view { + uint256 sum; + for (uint256 i; i < h.specCount(); ++i) { + (, PredgeValidatorBondV2.Status st,,,,, uint256 b,,) = bond.stakes(h.specs(i)); + if (st == PredgeValidatorBondV2.Status.Committed) sum += b; + else assertEq(b, 0, "closed => bond == 0"); + } + assertEq(bond.totalBonded(), sum); + } + + function invariant_totalEscrowedEqualsLiveEscrows() public view { + uint256 live; + uint256 paid; + for (uint256 i; i < h.specCount(); ++i) { + IAgentJobV2.Job memory j = job.getJob(h.jobIds(i)); + if (j.state == IAgentJobV2.State.Open || j.state == IAgentJobV2.State.Submitted) live += j.escrow; + else assertEq(j.escrow, 0, "settled => escrow == 0"); + if (j.state != IAgentJobV2.State.Open && j.state != IAgentJobV2.State.Submitted) {} + } + assertEq(job.totalEscrowed(), live); + paid = h.ghostEscrowedTotal() - live; + // everything ever escrowed is either live, or sits in credit, or was withdrawn: never double counted + assertLe(job.totalCredit(), paid); + } + + /// @dev Option (a): every verdict on record equals the on-chain truth, and the job settled the same way. + function invariant_everyRecordedVerdictIsTrue() public view { + for (uint256 i; i < h.specCount(); ++i) { + bytes32 spec = h.specs(i); + (, PredgeValidatorBondV2.Status st, uint8 score,,,,,,) = bond.stakes(spec); + if (st != PredgeValidatorBondV2.Status.Scored) continue; + IAgentJobV2.Job memory j = job.getJob(h.jobIds(i)); + bool delivered = j.hashedOnChain && j.deliverable == h.expectedOf(spec); + assertEq(score, delivered ? 100 : 0, "recorded verdict == truth"); + assertEq(uint8(j.state), uint8(delivered ? IAgentJobV2.State.Completed : IAgentJobV2.State.Rejected)); + } + } + + /// @dev Olympix 5.2.2: a stake closed without a verdict pays the provider iff the provider had submitted. + function invariant_silenceAfterSubmissionForfeits() public view { + for (uint256 i; i < h.specCount(); ++i) { + (, PredgeValidatorBondV2.Status st,,,,,,,) = bond.stakes(h.specs(i)); + uint64 submittedAt = job.getJob(h.jobIds(i)).submittedAt; + if (st == PredgeValidatorBondV2.Status.Forfeited) assertGt(submittedAt, 0, "forfeit only after a submission"); + if (st == PredgeValidatorBondV2.Status.Released) assertEq(submittedAt, 0, "returned only if nothing was submitted"); + } + } + + /// @dev Olympix 5.1.1: every stake commits to the value the client bound into the job's specHash. + function invariant_stakeCommitsToClientValue() public view { + for (uint256 i; i < h.specCount(); ++i) { + bytes32 spec = h.specs(i); + (, PredgeValidatorBondV2.Status st,,,,,, bytes32 c,) = bond.stakes(spec); + if (st == PredgeValidatorBondV2.Status.None) continue; + assertEq(c, keccak256(abi.encode(h.expectedOf(spec), h.saltOf(spec)))); + assertEq(bond.specHashFor(c), spec); + } + } + + function invariant_scoredNeverBeforeSubmission() public view { + for (uint256 i; i < h.specCount(); ++i) { + (, PredgeValidatorBondV2.Status st,,, uint64 scoredAt,,,,) = bond.stakes(h.specs(i)); + if ( + st == PredgeValidatorBondV2.Status.Committed || st == PredgeValidatorBondV2.Status.None + || st == PredgeValidatorBondV2.Status.Released || st == PredgeValidatorBondV2.Status.Forfeited + ) continue; + assertGe(scoredAt, job.getJob(h.jobIds(i)).submittedAt, "H-01: a verdict never precedes the submission"); + assertGt(job.getJob(h.jobIds(i)).submittedAt, 0); + } + } + + function invariant_deliverableImmutableOnceSubmitted() public view { + for (uint256 i; i < h.specCount(); ++i) { + IAgentJobV2.Job memory j = job.getJob(h.jobIds(i)); + if (j.state == IAgentJobV2.State.Open) assertEq(j.deliverable, bytes32(0)); + else if (j.state != IAgentJobV2.State.Expired) assertTrue(j.deliverable != bytes32(0)); + } + } + + function invariant_writeOnceAndMonotonic() public { + h.snapshot(); + assertFalse(h.violation(), "write-once / monotonic / only true verdicts recorded"); + } + + function invariant_livenessNoPermanentLock() public view { + // Every job past its latest possible deadline has been resolvable: it is either settled or claimable. + for (uint256 i; i < h.specCount(); ++i) { + IAgentJobV2.Job memory j = job.getJob(h.jobIds(i)); + if (j.state == IAgentJobV2.State.Open && block.timestamp >= j.expiredAt) { + // claimable by anyone: asserted structurally by the state machine; here just ensure the escrow is intact + assertGt(j.escrow, 0); + } + if (j.state == IAgentJobV2.State.Submitted && block.timestamp >= uint256(j.submittedAt) + job.evaluationWindow()) { + assertGt(j.escrow, 0); + } + } + } +} + +/// @dev Arbiter handler. +contract ArbiterHandler is Test { + PredgeRefundArbiterV2 public arb; + MockRefundProtocolV2 public rp; + address public safe = address(0x5AFE); + address public op = address(0x0A11); + address public payer = address(0xA1); + address public payee = address(0xA2); + uint256[] public pids; + mapping(uint256 => uint8) public prevStatus; + mapping(uint256 => bytes32) public firstEv; // payer evidence + mapping(uint256 => bytes32) public firstEvPayee; + uint256 public ghostRefundCalls; + bool public violation; + + constructor(PredgeRefundArbiterV2 a, MockRefundProtocolV2 r) { + arb = a; + rp = r; + vm.deal(op, type(uint128).max); + } + + function n() external view returns (uint256) { return pids.length; } + + function newPayment(uint96 amount) external { + amount = uint96(bound(amount, 1, 500 ether)); + pids.push(rp.recordPayment(payer, payee, amount, block.timestamp + 40 days)); + } + + function file(uint256 i, bool asPayer, bytes32 ev) external { + if (pids.length == 0) return; + uint256 pid = pids[bound(i, 0, pids.length - 1)]; + ev = ev == 0 ? bytes32(uint256(1)) : ev; + address who = asPayer ? payer : payee; + bytes32 before = arb.filedEvidence(pid, who); + vm.prank(who); + try arb.fileEvidence(pid, ev) {} catch {} + bytes32 afterV = arb.filedEvidence(pid, who); + if (before != 0 && afterV != before) violation = true; // write-once + } + + function ruleIt(uint256 i, bool fromPayer, bool refund) external { + if (pids.length == 0) return; + uint256 pid = pids[bound(i, 0, pids.length - 1)]; + address from = fromPayer ? payer : payee; + bytes32 ev = arb.filedEvidence(pid, from); + (, , uint256 amount,) = rp.paymentInfo(pid); + vm.prank(op); + try arb.rule{value: arb.requiredBond(amount)}(pid, from, ev, refund ? PredgeRefundArbiterV2.Direction.Refund : PredgeRefundArbiterV2.Direction.Release) {} catch {} + } + + function disputeIt(uint256 i) external { + if (pids.length == 0) return; + uint256 pid = pids[bound(i, 0, pids.length - 1)]; + address h = arb.harmedParty(pid); + vm.prank(h); + try arb.dispute(pid, bytes32(uint256(7))) {} catch {} + } + + function finalizeIt(uint256 i) external { + if (pids.length == 0) return; + uint256 pid = pids[bound(i, 0, pids.length - 1)]; + PredgeRefundArbiterV2.Ruling memory rl = arb.rulings(pid); + PredgeRefundArbiterV2.Direction d = rl.direction; + uint64 ruledAt = rl.ruledAt; + uint64 window = rl.window; + uint256 before = rp.refundCount(); + try arb.finalize(pid) { + if (block.timestamp < uint256(ruledAt) + window) violation = true; // never inside the window + if (d == PredgeRefundArbiterV2.Direction.Refund && rp.refundCount() != before + 1) violation = true; + } catch {} + } + + function resolveIt(uint256 i, bool refund) external { + if (pids.length == 0) return; + uint256 pid = pids[bound(i, 0, pids.length - 1)]; + vm.prank(safe); + try arb.resolve(pid, refund ? PredgeRefundArbiterV2.Direction.Refund : PredgeRefundArbiterV2.Direction.Release) {} catch {} + } + + function expireIt(uint256 i) external { + if (pids.length == 0) return; + try arb.expire(pids[bound(i, 0, pids.length - 1)]) {} catch {} + } + + function withdrawIt(uint8 who) external { + address a = who % 3 == 0 ? payer : who % 3 == 1 ? payee : op; + vm.prank(a); + try arb.withdraw() {} catch {} + } + + function warp(uint32 dt) external { + vm.warp(block.timestamp + bound(dt, 0, 10 days)); + } + + function snapshot() external { + for (uint256 i; i < pids.length; ++i) { + PredgeRefundArbiterV2.Status st = arb.rulings(pids[i]).status; + uint8 s8 = uint8(st); + if (s8 < prevStatus[pids[i]]) violation = true; + if (prevStatus[pids[i]] >= 3 && s8 != prevStatus[pids[i]]) violation = true; + prevStatus[pids[i]] = s8; + } + } +} + +contract ArbiterInvariantTest is StdInvariant, Test { + PredgeRefundArbiterV2 arb; + MockRefundProtocolV2 rp; + ArbiterHandler h; + + function setUp() public { + address predicted = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1); + rp = new MockRefundProtocolV2(predicted); + arb = new PredgeRefundArbiterV2(address(0x5AFE), address(0x0A11), address(rp), 1 days, 1e15, 500); + h = new ArbiterHandler(arb, rp); + targetContract(address(h)); + } + + function invariant_arbiterSolvent() public view { + assertGe(address(arb).balance, arb.totalBonded() + arb.totalCredit()); + } + + function invariant_totalBondedEqualsOpenRulings() public view { + uint256 sum; + uint256 rulings_; + uint256 refunds; + for (uint256 i; i < h.n(); ++i) { + PredgeRefundArbiterV2.Ruling memory rl = arb.rulings(h.pids(i)); + PredgeRefundArbiterV2.Status st = rl.status; + PredgeRefundArbiterV2.Direction d = rl.direction; + uint256 bond = rl.bond; + if (st == PredgeRefundArbiterV2.Status.None) continue; + rulings_++; + if (st == PredgeRefundArbiterV2.Status.Ruled || st == PredgeRefundArbiterV2.Status.Disputed) sum += bond; + else assertEq(bond, 0); + if (st == PredgeRefundArbiterV2.Status.Finalized && d == PredgeRefundArbiterV2.Direction.Refund) refunds++; + } + assertEq(arb.totalBonded(), sum); + assertEq(arb.rulingCount(), rulings_, "one ruling per payment"); + // refunds happen only via a Finalized Refund or an owner-chosen Refund after a dispute (Overturned/Finalized) + assertGe(rp.refundCount(), refunds); + assertLe(rp.refundCount(), rulings_); + } + + function invariant_everyRefundedPaymentWasFiledAndRuled() public view { + for (uint256 i; i < h.n(); ++i) { + (,,, , , bool refunded) = _refundedFlag(h.pids(i)); + if (!refunded) continue; + PredgeRefundArbiterV2.Status st = arb.rulings(h.pids(i)).status; + assertTrue(st == PredgeRefundArbiterV2.Status.Finalized || st == PredgeRefundArbiterV2.Status.Overturned, "refund only after a decision"); + } + } + + function _refundedFlag(uint256 pid) internal view returns (address, address, uint256, uint256, bool, bool) { + (address a, address b, uint256 c, uint256 d, bool r) = rp.payments(pid); + return (a, b, c, d, r, r); + } + + function invariant_writeOnceAndFinality() public { + h.snapshot(); + assertFalse(h.violation()); + } +} diff --git a/test/foundry-v2/Misc.t.sol b/test/foundry-v2/Misc.t.sol new file mode 100644 index 0000000..207eb0d --- /dev/null +++ b/test/foundry-v2/Misc.t.sol @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {PredgeAgentValidatorV2} from "../../contracts/v2/PredgeAgentValidatorV2.sol"; +import {PredgeSettlementV2} from "../../contracts/v2/PredgeSettlementV2.sol"; +import {MockRefundProtocolV2} from "../../contracts/v2/mocks/MockRefundProtocolV2.sol"; +import {Owned2Step, Operated} from "../../contracts/v2/access/PredgeAccess.sol"; +import {NoReceive} from "./Base.sol"; + +contract IdentityMock { + mapping(uint256 => address) public owners; + bool public broken; + function set(uint256 id, address o) external { owners[id] = o; } + function setBroken(bool b) external { broken = b; } + function ownerOf(uint256 id) external view returns (address) { + require(!broken && owners[id] != address(0), "ERC721: invalid token ID"); + return owners[id]; + } +} + +contract PredgeAgentValidatorV2Test is Test { + address safe = makeAddr("safe"); + address op = makeAddr("operator"); + address agentOwner = makeAddr("agentOwner"); + address stranger = makeAddr("stranger"); + PredgeAgentValidatorV2 reg; + PredgeAgentValidatorV2 regId; + IdentityMock idr; + + event ValidationRequest(address indexed validatorAddress, uint256 indexed agentId, string requestURI, bytes32 indexed requestHash); + event ValidationResponse(address indexed validatorAddress, uint256 indexed agentId, bytes32 indexed requestHash, uint8 response, string responseURI, bytes32 responseHash, string tag); + event RequestRecorded(bytes32 indexed requestHash, bytes32 digest, address indexed requester); + event ResponseRecorded(bytes32 indexed requestHash, address indexed responder, uint64 respondedAt); + + function setUp() public { + idr = new IdentityMock(); + idr.set(7, agentOwner); + reg = new PredgeAgentValidatorV2(safe, op, address(0)); + regId = new PredgeAgentValidatorV2(safe, op, address(idr)); + } + + function _req(PredgeAgentValidatorV2 r, uint256 agent, bytes32 d) internal returns (bytes32 h) { + vm.prank(op); + h = r.validationRequest(op, agent, "ipfs://req", d); + } + + function test_requestHash_isDomainSeparated() public { + bytes32 d = keccak256("d"); + assertEq(reg.requestHashFor(7, d), keccak256(abi.encode(block.chainid, address(reg), uint256(7), d))); + assertTrue(reg.requestHashFor(7, d) != regId.requestHashFor(7, d), "differs per registry"); + assertTrue(reg.requestHashFor(7, d) != reg.requestHashFor(8, d), "differs per agent"); + bytes32 onThisChain = reg.requestHashFor(7, d); + vm.chainId(999); + assertTrue(reg.requestHashFor(7, d) != onThisChain, "differs per chain"); + } + + /// @dev Olympix 5.3.3: an agent-NFT holder cannot pre-claim another agent's digest; the verdict lands on the right agent. + function test_request_digestCannotBeSquattedAcrossAgents() public { + bytes32 d = keccak256("victim digest"); + vm.prank(agentOwner); // owns agent 7, front-runs the digest under its own agent + bytes32 squat = regId.validationRequest(op, 7, "squat", d); + vm.prank(op); // the legitimate request for agent 1 still goes through, under its own key + bytes32 legit = regId.validationRequest(op, 1, "legit", d); + assertTrue(squat != legit); + vm.prank(op); + regId.validationResponse(legit, 100, "", keccak256("v"), ""); + bytes32[] memory hs = new bytes32[](2); + hs[0] = legit; hs[1] = squat; + (uint64 n1,) = regId.getSummary(1, hs); + (uint64 n7,) = regId.getSummary(7, hs); + assertEq(n1, 1, "verdict counted for the requesting agent"); + assertEq(n7, 0, "nothing credited to the squatter"); + } + + /// @dev Olympix 5.3.10: a pending request answered after a rotation records the key that answered. + function test_response_afterRotationNamesTheResponder() public { + bytes32 h = _req(reg, 1, keccak256("pending")); + address newOp = makeAddr("newOp"); + vm.prank(safe); + reg.setOperator(newOp); + vm.expectEmit(true, true, true, true); + emit ValidationResponse(newOp, 1, h, 100, "", keccak256("a"), ""); + vm.prank(newOp); + reg.validationResponse(h, 100, "", keccak256("a"), ""); + (address v,,,,,) = reg.getValidationStatus(h); + assertEq(v, newOp); + } + + function test_request_response_flow() public { + bytes32 d = keccak256("d"); + bytes32 h = reg.requestHashFor(7, d); + vm.expectEmit(true, true, true, true); + emit ValidationRequest(op, 7, "ipfs://req", h); + vm.expectEmit(true, true, false, true); + emit RequestRecorded(h, d, op); + assertEq(_req(reg, 7, d), h); + assertEq(reg.requestCount(), 1); + assertEq(reg.requestURIOf(h), "ipfs://req"); + assertFalse(reg.isValidated(h)); + assertEq(reg.commitLeadTime(h), 0); + + vm.warp(block.timestamp + 60); + vm.expectEmit(true, true, true, true); + emit ValidationResponse(op, 7, h, 100, "ipfs://resp", keccak256("a"), "tag"); + vm.expectEmit(true, true, false, true); + emit ResponseRecorded(h, op, uint64(block.timestamp)); + vm.prank(op); + reg.validationResponse(h, 100, "ipfs://resp", keccak256("a"), "tag"); + assertTrue(reg.isValidated(h)); + assertEq(reg.commitLeadTime(h), 60); + assertEq(reg.responseURIOf(h), "ipfs://resp"); + (address v, uint256 a, uint8 r, bytes32 rh, string memory tag, uint256 last) = reg.getValidationStatus(h); + assertEq(v, op); assertEq(a, 7); assertEq(r, 100); assertEq(rh, keccak256("a")); assertEq(tag, "tag"); assertEq(last, block.timestamp); + } + + function test_status_pendingShowsRequestTime() public { + bytes32 h = _req(reg, 1, keccak256("p")); + (,,,,, uint256 last) = reg.getValidationStatus(h); + assertEq(last, block.timestamp); + } + + function test_request_isRestricted_M05() public { + vm.prank(stranger); + vm.expectRevert(PredgeAgentValidatorV2.NotAuthorized.selector); + reg.validationRequest(op, 7, "x", keccak256("d")); + vm.prank(agentOwner); // no identity registry set: agent owners have no standing + vm.expectRevert(PredgeAgentValidatorV2.NotAuthorized.selector); + reg.validationRequest(op, 7, "x", keccak256("d")); + } + + function test_request_agentOwnerViaIdentityRegistry() public { + vm.prank(agentOwner); + bytes32 h = regId.validationRequest(op, 7, "x", keccak256("d")); + assertEq(h, regId.requestHashFor(7, keccak256("d"))); + vm.prank(agentOwner); // owner of agent 7 cannot request for agent 8 (unknown token -> ownerOf reverts) + vm.expectRevert(PredgeAgentValidatorV2.NotAuthorized.selector); + regId.validationRequest(op, 8, "x", keccak256("e")); + vm.prank(stranger); + vm.expectRevert(PredgeAgentValidatorV2.NotAuthorized.selector); + regId.validationRequest(op, 7, "x", keccak256("e")); + idr.setBroken(true); + vm.prank(agentOwner); + vm.expectRevert(PredgeAgentValidatorV2.NotAuthorized.selector); + regId.validationRequest(op, 7, "x", keccak256("e")); + } + + function test_request_reverts() public { + vm.startPrank(op); + vm.expectRevert(PredgeAgentValidatorV2.ZeroHash.selector); + reg.validationRequest(op, 1, "x", bytes32(0)); + vm.expectRevert(PredgeAgentValidatorV2.WrongValidator.selector); + reg.validationRequest(stranger, 1, "x", keccak256("d")); + reg.validationRequest(op, 1, "x", keccak256("d")); + vm.expectRevert(PredgeAgentValidatorV2.AlreadyRequested.selector); + reg.validationRequest(op, 1, "x", keccak256("d")); + vm.stopPrank(); + } + + function test_response_reverts() public { + bytes32 h = _req(reg, 1, keccak256("d")); + vm.prank(stranger); + vm.expectRevert(Operated.NotOperator.selector); + reg.validationResponse(h, 1, "", keccak256("a"), ""); + vm.startPrank(op); + vm.expectRevert(PredgeAgentValidatorV2.BadScore.selector); + reg.validationResponse(h, 101, "", keccak256("a"), ""); + vm.expectRevert(PredgeAgentValidatorV2.ZeroHash.selector); + reg.validationResponse(h, 1, "", bytes32(0), ""); + vm.expectRevert(PredgeAgentValidatorV2.NotRequested.selector); + reg.validationResponse(keccak256("never"), 1, "", keccak256("a"), ""); + reg.validationResponse(h, 0, "", keccak256("a"), ""); + vm.expectRevert(PredgeAgentValidatorV2.AlreadyResponded.selector); + reg.validationResponse(h, 1, "", keccak256("b"), ""); + vm.stopPrank(); + assertEq(reg.responseCount(), 1); + } + + function test_rotationNeverRewritesPastVerdict() public { + bytes32 h = _req(reg, 1, keccak256("d")); + vm.prank(op); + reg.validationResponse(h, 100, "", keccak256("a"), ""); + address newOp = makeAddr("new"); + vm.prank(safe); + reg.setOperator(newOp); + vm.prank(newOp); + vm.expectRevert(PredgeAgentValidatorV2.AlreadyResponded.selector); + reg.validationResponse(h, 0, "", keccak256("z"), ""); + (, , uint8 r, , ,) = reg.getValidationStatus(h); + assertEq(r, 100); + } + + function test_getSummary_dedupes_L06() public { + bytes32 a = _req(reg, 1, keccak256("a")); + bytes32 b = _req(reg, 1, keccak256("b")); + bytes32 c = _req(reg, 2, keccak256("c")); + bytes32 pending = _req(reg, 1, keccak256("p")); + vm.startPrank(op); + reg.validationResponse(a, 100, "", keccak256("1"), ""); + reg.validationResponse(b, 0, "", keccak256("2"), ""); + reg.validationResponse(c, 100, "", keccak256("3"), ""); + vm.stopPrank(); + bytes32[] memory hs = new bytes32[](7); + hs[0] = a; hs[1] = a; hs[2] = a; hs[3] = b; hs[4] = c; hs[5] = pending; hs[6] = keccak256("unknown"); + (uint64 n, uint8 avg) = reg.getSummary(1, hs); + assertEq(n, 2, "a counted once, not three times"); + assertEq(avg, 50); + (n, avg) = reg.getSummary(9, hs); + assertEq(n, 0); + assertEq(avg, 0); + } + + function test_getSummary_capped() public { + vm.expectRevert(PredgeAgentValidatorV2.TooManyHashes.selector); + reg.getSummary(1, new bytes32[](101)); + reg.getSummary(1, new bytes32[](100)); + } + + function testFuzz_summaryIgnoresDuplicates(uint8 dup) public { + dup = uint8(bound(dup, 1, 50)); + bytes32 a = _req(reg, 1, keccak256("a")); + vm.prank(op); + reg.validationResponse(a, 100, "", keccak256("1"), ""); + bytes32[] memory hs = new bytes32[](dup); + for (uint256 i; i < dup; ++i) hs[i] = a; + (uint64 n, uint8 avg) = reg.getSummary(1, hs); + assertEq(n, 1); + assertEq(avg, 100); + } +} + +contract AccessTest is Test { + address safe = makeAddr("safe"); + address op = makeAddr("operator"); + address next = makeAddr("next"); + PredgeSettlementV2 s; + PredgeAgentValidatorV2 reg; + + event OwnershipTransferStarted(address indexed previousOwner, address indexed pendingOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + event OperatorUpdated(address indexed previousOperator, address indexed newOperator); + + function setUp() public { + s = new PredgeSettlementV2(safe); + reg = new PredgeAgentValidatorV2(safe, op, address(0)); + } + + function mkS(address o) external returns (address) { return address(new PredgeSettlementV2(o)); } + function mkR(address o, address p) external returns (address) { return address(new PredgeAgentValidatorV2(o, p, address(0))); } + + function test_constructorZeroOwnerOrOperator() public { + vm.expectRevert(Owned2Step.ZeroAddress.selector); + this.mkS(address(0)); + vm.expectRevert(Owned2Step.ZeroAddress.selector); + this.mkR(address(0), op); + vm.expectRevert(Owned2Step.ZeroAddress.selector); + this.mkR(safe, address(0)); + } + + function test_deployerHoldsNoRole() public view { + assertEq(s.owner(), safe); + assertTrue(s.owner() != address(this)); + assertEq(reg.operator(), op); + } + + function test_twoStepOwnership() public { + vm.prank(op); + vm.expectRevert(Owned2Step.NotOwner.selector); + s.transferOwnership(next); + + vm.expectEmit(true, true, false, false); + emit OwnershipTransferStarted(safe, next); + vm.prank(safe); + s.transferOwnership(next); + assertEq(s.owner(), safe, "nomination alone changes nothing"); + assertEq(s.pendingOwner(), next); + + vm.prank(op); + vm.expectRevert(Owned2Step.NotPendingOwner.selector); + s.acceptOwnership(); + + vm.expectEmit(true, true, false, false); + emit OwnershipTransferred(safe, next); + vm.prank(next); + s.acceptOwnership(); + assertEq(s.owner(), next); + assertEq(s.pendingOwner(), address(0)); + + vm.prank(safe); + vm.expectRevert(Owned2Step.NotOwner.selector); + s.transferOwnership(safe); + } + + function test_nominationCanBeCancelled() public { + vm.startPrank(safe); + s.transferOwnership(next); + s.transferOwnership(address(0)); + vm.stopPrank(); + vm.prank(next); + vm.expectRevert(Owned2Step.NotPendingOwner.selector); + s.acceptOwnership(); + } + + function test_noRenounce() public { + (bool ok,) = address(s).call(abi.encodeWithSignature("renounceOwnership()")); + assertFalse(ok, "there is no renounceOwnership"); + assertEq(s.owner(), safe); + } + + function test_setOperator() public { + address n = makeAddr("n"); + vm.prank(op); + vm.expectRevert(Owned2Step.NotOwner.selector); + reg.setOperator(n); + vm.startPrank(safe); + vm.expectRevert(Owned2Step.ZeroAddress.selector); + reg.setOperator(address(0)); + vm.expectEmit(true, true, false, false); + emit OperatorUpdated(op, n); + reg.setOperator(n); + vm.stopPrank(); + assertEq(reg.operator(), n); + vm.prank(op); + vm.expectRevert(Operated.NotOperator.selector); + reg.validationResponse(bytes32(uint256(1)), 1, "", bytes32(uint256(1)), ""); + } +} + +contract PredgeSettlementV2Test is Test { + address safe = makeAddr("safe"); + address payer = makeAddr("payer"); + PredgeSettlementV2 s; + + event Paid(address indexed payer, bytes32 indexed route, uint256 amount, uint256 timestamp, string meta); + event Withdrawn(address indexed to, uint256 amount); + + function setUp() public { + s = new PredgeSettlementV2(safe); + vm.deal(payer, 10 ether); + } + + function test_pay_and_receive() public { + vm.expectEmit(true, true, false, true); + emit Paid(payer, bytes32("r"), 5, block.timestamp, "meta"); + vm.prank(payer); + s.payForRoute{value: 5}(bytes32("r"), "meta"); + vm.expectEmit(true, true, false, true); + emit Paid(payer, bytes32("direct"), 7, block.timestamp, ""); + vm.prank(payer); + (bool ok,) = address(s).call{value: 7}(""); + assertTrue(ok); + assertEq(address(s).balance, 12); + } + + function test_zeroPaymentReverts() public { + vm.expectRevert(PredgeSettlementV2.ZeroPayment.selector); + s.payForRoute(bytes32("r"), ""); + (bool ok, bytes memory ret) = address(s).call(""); + assertFalse(ok); + assertEq(bytes4(ret), PredgeSettlementV2.ZeroPayment.selector); + } + + function test_withdraw_ownerOnly_eventBeforeCall() public { + vm.prank(payer); + s.payForRoute{value: 9}(bytes32("r"), ""); + vm.prank(payer); + vm.expectRevert(Owned2Step.NotOwner.selector); + s.withdraw(payable(payer)); + vm.expectEmit(true, false, false, true); + emit Withdrawn(safe, 9); + vm.prank(safe); + s.withdraw(payable(safe)); + assertEq(safe.balance, 9); + assertEq(address(s).balance, 0); + } + + function test_withdraw_zeroAddressRejected_L05() public { + vm.prank(payer); + s.payForRoute{value: 9}(bytes32("r"), ""); + vm.prank(safe); + vm.expectRevert(Owned2Step.ZeroAddress.selector); + s.withdraw(payable(address(0))); + assertEq(address(s).balance, 9, "nothing burned"); + } + + function test_withdraw_failedTransferReverts() public { + NoReceive bad = new NoReceive(); + vm.prank(safe); + vm.expectRevert(PredgeSettlementV2.WithdrawFailed.selector); + s.withdraw(payable(address(bad))); + } + + function test_forcedValueIsWithdrawable() public { + vm.deal(address(s), 3 ether); // simulates forced (selfdestruct / coinbase) value + vm.prank(safe); + s.withdraw(payable(safe)); + assertEq(safe.balance, 3 ether); + } +} + +contract MockRefundProtocolV2Test is Test { + MockRefundProtocolV2 m; + address arbiter = makeAddr("arbiter"); + address a = makeAddr("a"); + address b = makeAddr("b"); + + function setUp() public { + m = new MockRefundProtocolV2(arbiter); + } + + function mkM(address a_) external returns (address) { return address(new MockRefundProtocolV2(a_)); } + + function test_zeroArbiter() public { + vm.expectRevert(MockRefundProtocolV2.ZeroAddress.selector); + this.mkM(address(0)); + } + + function test_record_and_info() public { + vm.expectRevert(MockRefundProtocolV2.ZeroAddress.selector); + m.recordPayment(address(0), b, 1, 1); + vm.expectRevert(MockRefundProtocolV2.ZeroAddress.selector); + m.recordPayment(a, address(0), 1, 1); + uint256 id = m.recordPayment(a, b, 5, 100); + (address p, address q, uint256 amt, uint256 rel) = m.paymentInfo(id); + assertEq(p, a); assertEq(q, b); assertEq(amt, 5); assertEq(rel, 100); + } + + function test_refund_guards() public { + uint256 id = m.recordPayment(a, b, 5, block.timestamp + 10); + vm.expectRevert(MockRefundProtocolV2.CallerNotArbiter.selector); + m.refundByArbiter(id); + vm.startPrank(arbiter); + vm.expectRevert(MockRefundProtocolV2.UnknownPayment.selector); + m.refundByArbiter(99); + m.refundByArbiter(id); + vm.expectRevert(MockRefundProtocolV2.AlreadyRefunded.selector); + m.refundByArbiter(id); + uint256 id2 = m.recordPayment(a, b, 5, block.timestamp + 10); + vm.warp(block.timestamp + 10); + vm.expectRevert(MockRefundProtocolV2.AlreadyReleased.selector); + m.refundByArbiter(id2); + vm.stopPrank(); + assertEq(m.refundCount(), 1); + } + + function test_lockup_arbiterOnly() public { + vm.expectRevert(MockRefundProtocolV2.CallerNotArbiter.selector); + m.setLockupSeconds(a, 1); + vm.prank(arbiter); + m.setLockupSeconds(a, 1); + assertEq(m.lockupSeconds(a), 1); + } +} diff --git a/test/foundry-v2/OlympixRegressions.t.sol b/test/foundry-v2/OlympixRegressions.t.sol new file mode 100644 index 0000000..0359d4c --- /dev/null +++ b/test/foundry-v2/OlympixRegressions.t.sol @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Base} from "./Base.sol"; +import {ArbiterBase} from "./PredgeRefundArbiterV2.t.sol"; +import {PredgeValidatorBondV2} from "../../contracts/v2/PredgeValidatorBondV2.sol"; +import {PredgeRefundArbiterV2} from "../../contracts/v2/PredgeRefundArbiterV2.sol"; +import {IAgentJobV2} from "../../contracts/v2/interfaces/IAgentJobV2.sol"; + +/// @notice Regression tests for the Olympix BugPoCer scan of c47ed42 (23 Oct 2026). Each test replays +/// the attack sequence of the corresponding Olympix PoC against the fixed contracts and asserts +/// the harm assertions of that PoC now hold (or, where the attack's first step is now refused, +/// that it reverts and the closest still-possible variant is harmless). +contract OlympixBondRegressionTest is Base { + bytes32 internal constant SALT = keccak256("client salt"); + bytes32 internal constant AGREED = keccak256("operator-and-provider-agreed hash"); + uint256 internal constant ESCROW = 10 ether; + bytes32 internal EXPECTED; // sha256(GOOD): fixed by the client in the specHash + + function setUp() public { + _deployJobAndBond(); + EXPECTED = _good(); + } + + // ------------------------------------------------------------ 5.1.1 (a, b, c) + + /// @dev 5.1.1-a/b step 1: the operator can no longer commit to a value of its own choosing. + function test_5_1_1_operatorCannotChooseExpected() public { + (uint256 id, bytes32 spec) = _newJob(ESCROW, EXPECTED, SALT); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.CommitmentNotBound.selector); + bond.stakeAndCommit{value: ESCROW}(spec, _commitment(AGREED, SALT), id); + // Not even with a spec derived for its own value: that spec belongs to no job the client created. + bytes32 own = _bspec(AGREED, SALT); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.JobNotBound.selector); + bond.stakeAndCommit{value: ESCROW}(own, _commitment(AGREED, SALT), id); + } + + /// @dev 5.1.1-a/c, strongest remaining variant: the operator commits to the client's value (it must) and + /// leaks it to a colluding provider, who submits the right hash with NO bytes. With option (a) the + /// false pass is refused outright: the escrow never moves, whatever the bond size. + function test_5_1_1_leakedExpectedWithoutBytesCannotPass() public { + (uint256 id, bytes32 spec) = _newJob(ESCROW, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, ESCROW); + _submit(id, EXPECTED); // claimed hash only: no work, no bytes + assertFalse(job.getJob(id).hashedOnChain); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.VerdictContradictsEvidence.selector); + bond.recordScore(spec, 100, EXPECTED, SALT, 0); + assertEq(job.credit(provider), 0, "the escrow did not move"); + _score(spec, 0, EXPECTED, SALT); // the only verdict the operator can record + assertEq(job.credit(client), ESCROW, "client refunded"); + assertEq(job.credit(provider), 0); + } + + /// @dev 5.1.1-b false fail: the operator cannot commit an unmatchable value, and with the client's value a 0 + /// on real bytes is refused; the provider is paid. + function test_5_1_1_falseFailOnRealWorkRefused() public { + (uint256 id, bytes32 spec) = _newJob(ESCROW, EXPECTED, SALT); + bytes32 unmatchable = keccak256("garbage the operator picked"); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.CommitmentNotBound.selector); + bond.stakeAndCommit{value: ESCROW}(spec, _commitment(unmatchable, SALT), id); + + _stake(spec, EXPECTED, SALT, id, ESCROW); + _deliver(id, GOOD); + assertTrue(job.getJob(id).hashedOnChain); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.VerdictContradictsEvidence.selector); + bond.recordScore(spec, 0, EXPECTED, SALT, 0); + _score(spec, 100, EXPECTED, SALT); + assertEq(job.credit(provider), ESCROW, "provider paid"); + } + + /// @dev 5.1.2 (High, bond not collateralised against escrow): with option (a) the bond size no longer matters for + /// correctness. At the shipped bondBps = 0, a colluding operator and provider cannot record a false pass on a + /// mismatching on-chain deliverable, for any escrow. + function testFuzz_5_1_2_falsePassImpossibleAtShippedParams(uint96 escrow, bytes calldata wrong) public { + vm.assume(wrong.length != 0 && sha256(wrong) != EXPECTED); + escrow = uint96(bound(escrow, 1, 1_000 ether)); + vm.prank(safe); + bond.setParams(MIN_BOND, 0); + (uint256 id, bytes32 spec) = _newJob(escrow, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, MIN_BOND); + _deliver(id, wrong); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.VerdictContradictsEvidence.selector); + bond.recordScore(spec, 100, EXPECTED, SALT, 0); + assertEq(job.credit(provider), 0); + assertEq(job.totalEscrowed(), escrow); + } + + // ------------------------------------------------------------ 5.2.2 (a, b, c, d, e) + + /// @dev 5.2.2-a, shipped parameters (bondBps = 0): silence on a delivered job now costs the operator its bond, + /// and the provider receives it. + function test_5_2_2_silenceForfeitsFloorBondToProvider() public { + vm.prank(safe); + bond.setParams(MIN_BOND, 0); + (uint256 id, bytes32 spec) = _newJob(100 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, MIN_BOND); + _deliver(id, GOOD); + vm.warp(block.timestamp + EVAL_WINDOW); + job.claimRefund(id); + bond.release(spec); + assertEq(bond.credit(op), 0, "silence is not free"); + assertEq(bond.credit(provider), MIN_BOND, "the provider is compensated with the bond"); + assertEq(job.credit(client), 100 ether, "documented timeout policy: escrow to the client"); + } + + /// @dev 5.2.2-a second test / 5.2.2-b: at full bond the provider receives a bond equal to the escrow. + function test_5_2_2_silenceForfeitsFullBondToProvider() public { + (uint256 id, bytes32 spec) = _newJob(5 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 5 ether); + _deliver(id, GOOD); + vm.warp(block.timestamp + EVAL_WINDOW); + vm.prank(op); + vm.expectRevert(); // a late verdict is impossible (job: PastDeadline) + bond.recordScore(spec, 100, EXPECTED, SALT, 0); + job.claimRefund(id); + bond.release(spec); + assertLt(bond.credit(op), 5 ether); + assertEq(bond.credit(provider), 5 ether); + uint256 before_ = provider.balance; + vm.prank(provider); + bond.withdraw(); + assertEq(provider.balance - before_, 5 ether); + } + + /// @dev Unchanged and intended: an operator that never staked, or a job never submitted, returns the bond. + function test_5_2_2_noSubmissionStillReturnsBond() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 1 ether); + vm.warp(block.timestamp + 7 days); + job.claimRefund(id); + bond.release(spec); + assertEq(bond.credit(op), 1 ether); + assertEq(bond.credit(provider), 0); + } + + // ------------------------------------------------------------ 5.2.3 / 5.1.2-a + + /// @dev 5.2.3: the operator acting as the job's client cannot stake on (or later score) that job. + function test_5_2_3_operatorAsClientRefused() public { + bytes32 spec = _bspec(EXPECTED, SALT); + vm.prank(op); + uint256 id = job.createJob{value: 1 ether}(provider, address(bond), spec, uint64(block.timestamp + 1 days)); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.ClientIsOperator.selector); + bond.stakeAndCommit{value: 1 ether}(spec, _commitment(EXPECTED, SALT), id); + } + + // ------------------------------------------------------------ 5.3.8 + + /// @dev 5.3.8: rotating the operator mid-flight without handing over the preimage no longer leaves the provider + /// with nothing: the stake is forfeited to it. + function test_5_3_8_rotationWithoutPreimageForfeitsToProvider() public { + (uint256 id, bytes32 spec) = _newJob(2 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 2 ether); + _deliver(id, GOOD); + address newOp = makeAddr("newOp"); + vm.prank(safe); + bond.setOperator(newOp); + vm.prank(newOp); // does not know the salt + vm.expectRevert(PredgeValidatorBondV2.BadReveal.selector); + bond.recordScore(spec, 100, EXPECTED, keccak256("guess"), 0); + vm.warp(block.timestamp + EVAL_WINDOW); + job.claimRefund(id); + bond.release(spec); + assertEq(bond.credit(provider), 2 ether); + assertEq(bond.credit(op), 0); + } +} + +contract OlympixArbiterRegressionTest is ArbiterBase { + PredgeRefundArbiterV2.Direction internal constant REFUND = PredgeRefundArbiterV2.Direction.Refund; + PredgeRefundArbiterV2.Direction internal constant RELEASE = PredgeRefundArbiterV2.Direction.Release; + + function setUp() public { + _deployArbiter(); + } + + /// @dev 5.2.1-a/b/d: the PoC's payment (lockup ends 1 hour after the window) can no longer be ruled on in + /// either direction, so a free dispute can never run a ruling past the release. + function test_5_2_1_shortLockupCannotBeRuled() public { + uint256 pid = rp.recordPayment(payer, payee, 1 ether, block.timestamp + WINDOW + 1 hours); + _file(pid); + uint256 need = arb.requiredBond(1 ether); + vm.startPrank(op); + vm.expectRevert(PredgeRefundArbiterV2.LockupTooShort.selector); + arb.rule{value: need}(pid, payer, EV_PAYER, REFUND); + vm.expectRevert(PredgeRefundArbiterV2.LockupTooShort.selector); + arb.rule{value: need}(pid, payee, EV_PAYEE, RELEASE); + vm.stopPrank(); + } + + /// @dev 5.2.1-a replayed at the tightest lockup that is now allowed: the payee disputes one second before the + /// window closes and the owner resolves at the very last second of its resolution period. The refund + /// still executes, because the whole timeline ends before releaseTimestamp. + function test_5_2_1_lastSecondDisputeStillLeavesOwnerFullPeriod() public { + uint256 pid = rp.recordPayment(payer, payee, 1 ether, block.timestamp + WINDOW + arb.RESOLUTION_TIMEOUT() + 1); + _file(pid); + uint256 need = arb.requiredBond(1 ether); + _rule(pid, payer, EV_PAYER, REFUND, need); + uint256 ruledAt = block.timestamp; + vm.warp(ruledAt + WINDOW - 1); + vm.prank(payee); + arb.dispute(pid, EV_COUNTER); + uint64 expiresAt = arb.rulings(pid).expiresAt; + (,,, uint256 releaseTs) = rp.paymentInfo(pid); + assertLt(expiresAt, releaseTs, "the owner's whole resolution period ends before release"); + vm.warp(uint256(expiresAt) - 1); + vm.prank(safe); + arb.resolve(pid, REFUND); + (,,,, bool refunded) = rp.payments(pid); + assertTrue(refunded, "the correct refund executes"); + assertEq(arb.credit(op), need); + } + + /// @dev 5.2.1-b: a Release ruling overturned into a Refund at the last moment still executes and slashes. + function test_5_2_1_releaseOverturnStillExecutes() public { + uint256 pid = rp.recordPayment(payer, payee, 1 ether, block.timestamp + WINDOW + arb.RESOLUTION_TIMEOUT() + 1); + _file(pid); + uint256 need = arb.requiredBond(1 ether); + _rule(pid, payee, EV_PAYEE, RELEASE, need); + vm.warp(block.timestamp + WINDOW - 1); + vm.prank(payer); + arb.dispute(pid, EV_COUNTER); + vm.warp(uint256(arb.rulings(pid).expiresAt) - 1); + vm.prank(safe); + arb.resolve(pid, REFUND); + assertEq(arb.credit(payer), need, "operator's bond goes to the harmed payer"); + assertEq(rp.refundCount(), 1); + } +} diff --git a/test/foundry-v2/Pocs.t.sol b/test/foundry-v2/Pocs.t.sol new file mode 100644 index 0000000..8bc5ecf --- /dev/null +++ b/test/foundry-v2/Pocs.t.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Base} from "./Base.sol"; +import {ArbiterBase} from "./PredgeRefundArbiterV2.t.sol"; +import {PredgeValidatorBondV2} from "../../contracts/v2/PredgeValidatorBondV2.sol"; +import {PredgeRefundArbiterV2} from "../../contracts/v2/PredgeRefundArbiterV2.sol"; +import {IAgentJobV2} from "../../contracts/v2/interfaces/IAgentJobV2.sol"; +import {AgentJob as AgentJobV1} from "../../contracts/AgentJob.sol"; +import {PredgeValidatorBond as BondV1} from "../../contracts/PredgeValidatorBond.sol"; +import {PredgeRefundArbiter as ArbiterV1} from "../../contracts/PredgeRefundArbiter.sol"; +import {MockRefundProtocol as MockRPV1} from "../../contracts/MockRefundProtocol.sol"; + +/// @notice Proof-of-concept tests for the self-audit findings. Each finding has two tests: the exact +/// v1 attack sequence run against the v1 contracts (it SUCCEEDS there, which proves the PoC +/// is a real exploit) and the same sequence against v2 (it reverts or is harmless). +contract PocsBondTest is Base { + bytes32 internal E; // sha256(GOOD): the client's expected output hash + bytes32 constant SALT = keccak256("salt"); + bytes32 constant X = keccak256("not the expected thing"); + + function setUp() public { + _deployJobAndBond(); + E = _good(); + } + + // ------------------------------------------------------------------ H-01 + + /// @dev v1: score a verdict BEFORE the provider submits, wait out the window, reclaim, then let + /// the provider submit something that contradicts the verdict. The lie is provable but closed. + function test_v1_H01_verdictBeforeSubmissionIsReclaimedUnchallengeable() public { + AgentJobV1 j1 = new AgentJobV1(); + BondV1 b1 = new BondV1(op, address(j1), 1 days); + bytes32 spec = keccak256("spec"); + vm.prank(client); + uint256 id = j1.createJob{value: 1 ether}(provider, op, spec); + vm.startPrank(op); + b1.stakeAndCommit{value: 1 ether}(spec, E, id); + b1.recordScore(spec, 100); // verdict recorded with NO deliverable in existence + vm.warp(block.timestamp + 1 days); + b1.reclaim(spec); // bond back, nothing was ever challengeable + vm.stopPrank(); + vm.prank(provider); + j1.submit(id, X, ""); // the deliverable now contradicts the verdict + vm.expectRevert(BondV1.Closed.selector); + b1.challenge(spec); // provably false, permanently closed + } + + /// @dev v2: the same sequence cannot even start. Scoring needs a submission, so nothing can be + /// scored before the deliverable exists, and the bond only comes back with a verdict or on expiry. + function test_v2_H01_verdictBeforeSubmissionReverts() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, E, SALT); + _stake(spec, E, SALT, id, 1 ether); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.NotSubmitted.selector); + bond.recordScore(spec, 100, E, SALT, 0); + vm.warp(block.timestamp + 1 days); + vm.expectRevert(PredgeValidatorBondV2.JobNotExpired.selector); + bond.release(spec); // no way to get the bond back without a verdict while the job runs + // The only exit is the job expiring with NO verdict ever given, which lies about nothing. + vm.warp(block.timestamp + 7 days); + job.claimRefund(id); + bond.release(spec); + assertEq(job.credit(client), 1 ether, "client got the escrow back"); + } + + /// @dev v2: a late submission is still judged against its own bytes; the lie the v1 PoC recorded is refused. + function test_v2_H01_lateSubmissionStillJudgedOnEvidence() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, E, SALT); + _stake(spec, E, SALT, id, 1 ether); + vm.warp(block.timestamp + 2 days); + _submit(id, X); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.VerdictContradictsEvidence.selector); + bond.recordScore(spec, 100, E, SALT, 0); // the lie + _score(spec, 0, E, SALT); + (,,,, uint64 scoredAt,,,,) = bond.stakes(spec); + assertGe(scoredAt, job.getJob(id).submittedAt); + assertEq(job.credit(client), 1 ether); + } + + // ------------------------------------------------------------------ H-02 + + /// @dev v1: `expected` is public from the moment of the stake. The provider submits it with no work; + /// an honest score of 0 is then slashable. (And committing after submission, as the v1 smoke test + /// does, lets the validator copy the deliverable.) + function test_v1_H02_providerCopiesPublicExpectedAndHonestFailBecomesSlashable() public { + AgentJobV1 j1 = new AgentJobV1(); + BondV1 b1 = new BondV1(op, address(j1), 1 days); + bytes32 spec = keccak256("spec"); + vm.prank(client); + uint256 id = j1.createJob{value: 1 ether}(provider, op, spec); + vm.prank(op); + b1.stakeAndCommit{value: 1 ether}(spec, E, id); + (bytes32 leaked,,,,,,,) = b1.stakes(spec); // anyone can read it + assertEq(leaked, E); + vm.prank(provider); + j1.submit(id, leaked, ""); // no work, just the copied answer + vm.prank(op); + b1.recordScore(spec, 0); // the validator's honest view: nothing was really produced + assertTrue(b1.wouldSlash(spec), "an honest-by-intent verdict is now slashable"); + vm.prank(stranger); + b1.challenge(spec); // and the bond is taken + } + + /// @dev v2: the committed value is a hash, so there is nothing to copy. + function test_v2_H02_expectedIsHiddenSoItCannotBeCopied() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, E, SALT); + _stake(spec, E, SALT, id, 1 ether); + (,,,,,,, bytes32 commitment, bytes32 expected) = bond.stakes(spec); + assertEq(expected, bytes32(0)); + assertEq(commitment, keccak256(abi.encode(E, SALT))); + // The best the provider can do with public data is submit the commitment itself. + _submit(id, commitment); + _score(spec, 0, E, SALT); // honest: the deliverable is not the expected hash, and nothing can overturn it + assertEq(bond.credit(op), 1 ether, "the honest operator keeps its bond"); + } + + /// @dev v2: the operator cannot choose `expected` after seeing the deliverable. + function test_v2_H02_cannotCommitAfterSubmission() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, E, SALT); + _submit(id, X); + vm.startPrank(op); + // committing to the deliverable it has just seen is impossible twice over: the client fixed `expected`... + vm.expectRevert(PredgeValidatorBondV2.CommitmentNotBound.selector); + bond.stakeAndCommit{value: 1 ether}(spec, keccak256(abi.encode(X, SALT)), id); + // ...and even the client's own commitment can no longer be staked once a deliverable exists + vm.expectRevert(PredgeValidatorBondV2.NotOpen.selector); + bond.stakeAndCommit{value: 1 ether}(spec, keccak256(abi.encode(E, SALT)), id); + vm.stopPrank(); + } + + /// @dev v2: a reveal that does not match the commitment is rejected, so `expected` cannot be swapped. + function test_v2_H02_cannotSwapExpectedAtReveal() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, E, SALT); + _stake(spec, E, SALT, id, 1 ether); + _submit(id, X); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.BadReveal.selector); + bond.recordScore(spec, 100, X, SALT, 0); // would make the false pass look honest + } + + // ------------------------------------------------------------------ M-02 / M-04 + + /// @dev M-02 (v1: slash paid the caller). v2 has no challenge at all: the false pass a self-challenge would have + /// "punished" cannot be recorded in the first place. + function test_v2_M02_falsePassCannotBeRecorded() public { + (uint256 id, bytes32 spec) = _newJob(5 ether, E, SALT); + _stake(spec, E, SALT, id, 5 ether); + _submit(id, X); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.VerdictContradictsEvidence.selector); + bond.recordScore(spec, 100, E, SALT, 0); + assertEq(job.credit(provider), 0); + } + + /// @dev M-04: the verdict IS the settlement; there is no way to record a verdict that does not move the job. + function test_v2_M04_verdictMovesTheMoney() public { + (uint256 id, bytes32 spec) = _newJob(2 ether, E, SALT); + _stake(spec, E, SALT, id, 2 ether); + _deliver(id, GOOD); + _score(spec, 100, E, SALT); + assertEq(uint8(job.getJob(id).state), uint8(IAgentJobV2.State.Completed)); + // No other party can settle this job: the bond contract is the only evaluator. + vm.prank(op); + vm.expectRevert(); + job.reject(id, 0, ""); + } + + /// @dev A colluding operator and provider cannot extract the escrow with a false pass, at ANY bond ratio. + function testFuzz_collusionCannotExtractEscrowAtAnyBond(uint96 escrow, uint16 bps) public { + escrow = uint96(bound(escrow, 1, 100 ether)); + bps = uint16(bound(bps, 0, 10_000)); + vm.prank(safe); + bond.setParams(MIN_BOND, bps); + (uint256 id, bytes32 spec) = _newJob(escrow, E, SALT); + uint256 b = bond.requiredBond(escrow); + _stake(spec, E, SALT, id, b); + _submit(id, X); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.VerdictContradictsEvidence.selector); + bond.recordScore(spec, 100, E, SALT, 0); + _score(spec, 0, E, SALT); + assertEq(job.credit(provider), 0, "colluders extracted nothing"); + assertEq(job.credit(client), escrow, "client is made whole"); + } +} + +contract PocsArbiterTest is ArbiterBase { + PredgeRefundArbiterV2.Direction constant REFUND = PredgeRefundArbiterV2.Direction.Refund; + PredgeRefundArbiterV2.Direction constant RELEASE = PredgeRefundArbiterV2.Direction.Release; + bytes32 constant EV = keccak256("fabricated evidence"); + + function setUp() public { + _deployArbiter(); + } + + /// @dev v1 (a): any key can file "evidence" for any payment id, and the operator can cite it. + /// (b) the refund executes inside rule(), before any window. (c) challenge can never succeed. + function test_v1_H03_arbitraryRefundAtGasCostAndUnslashable() public { + // arbiter v1 is deployed with owner = this contract; the mock needs the arbiter address. + address predicted = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1); + MockRPV1 rp1 = new MockRPV1(predicted); + ArbiterV1 a1 = new ArbiterV1(address(rp1), op, 1 days, 1e15); + assertEq(address(a1), predicted); + uint256 victimPayment = 4242; + vm.prank(stranger); // not a party to anything + a1.fileEvidence(victimPayment, EV); + vm.prank(op); + a1.rule{value: 1e15}(victimPayment, stranger, EV, ArbiterV1.Direction.Refund); + assertTrue(rp1.refunded(victimPayment), "refund executed immediately, on a stranger's say-so"); + assertFalse(a1.wouldSlash(victimPayment)); + vm.expectRevert(); + a1.challenge(victimPayment); // RulingHonest, always + vm.warp(block.timestamp + 1 days); + vm.prank(op); + a1.reclaim(victimPayment); // bond returned: the whole thing cost gas + } + + /// @dev v2 (a): a stranger cannot file evidence for someone else's payment, and cannot be cited. + function test_v2_H03_evidenceBoundToPaymentParties() public { + uint256 pid = _pay(1 ether); + vm.prank(stranger); + vm.expectRevert(PredgeRefundArbiterV2.NotAParty.selector); + arb.fileEvidence(pid, EV); + vm.prank(op); + vm.expectRevert(PredgeRefundArbiterV2.NotAParty.selector); + arb.rule{value: 1 ether}(pid, stranger, EV, REFUND); + // and a payment id that does not exist cannot be ruled on at all + vm.prank(op); + vm.expectRevert(PredgeRefundArbiterV2.UnknownPayment.selector); + arb.rule{value: 1 ether}(4242, stranger, EV, REFUND); + } + + /// @dev v2 (b): nothing executes inside rule(); the refund only happens after the window. + function test_v2_H03_noRefundInsideRule() public { + uint256 pid = _pay(1 ether); + _file(pid); + _rule(pid, payer, EV_PAYER, REFUND, arb.requiredBond(1 ether)); + assertEq(rp.refundCount(), 0); + vm.warp(block.timestamp + WINDOW - 1); + vm.expectRevert(PredgeRefundArbiterV2.WindowOpen.selector); + arb.finalize(pid); + assertEq(rp.refundCount(), 0); + } + + /// @dev v2 (c): a reachable challenge path. The harmed payee freezes a wrongful refund; the Safe + /// overturns; the refund never executes and the operator's bond goes to the payee. + function test_v2_H03_wrongfulRefundIsStoppedAndSlashed() public { + uint256 pid = _pay(10 ether); + _file(pid); + uint256 bondAmt = arb.requiredBond(10 ether); + _rule(pid, payer, EV_PAYER, REFUND, bondAmt); + vm.prank(payee); + arb.dispute(pid, EV_COUNTER); + vm.prank(safe); + arb.resolve(pid, RELEASE); + assertEq(rp.refundCount(), 0, "the wrongful refund never executed"); + assertEq(arb.credit(payee), bondAmt, "operator's bond paid to the harmed payee"); + assertEq(arb.credit(op), 0); + } + + /// @dev v2: a refund cannot be forced inside the window by anyone, in any order of calls. + function testFuzz_v2_refundOnlyAfterWindowOrOwnerDecision(uint32 dt) public { + uint256 pid = _pay(1 ether); + _file(pid); + _rule(pid, payer, EV_PAYER, REFUND, arb.requiredBond(1 ether)); + dt = uint32(bound(dt, 0, WINDOW - 1)); + vm.warp(block.timestamp + dt); + vm.expectRevert(PredgeRefundArbiterV2.WindowOpen.selector); + arb.finalize(pid); + vm.prank(op); + vm.expectRevert(); + arb.expire(pid); + assertEq(rp.refundCount(), 0); + } +} diff --git a/test/foundry-v2/PredgeRefundArbiterV2.t.sol b/test/foundry-v2/PredgeRefundArbiterV2.t.sol new file mode 100644 index 0000000..3940c03 --- /dev/null +++ b/test/foundry-v2/PredgeRefundArbiterV2.t.sol @@ -0,0 +1,526 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {PredgeRefundArbiterV2} from "../../contracts/v2/PredgeRefundArbiterV2.sol"; +import {MockRefundProtocolV2} from "../../contracts/v2/mocks/MockRefundProtocolV2.sol"; +import {Owned2Step, Operated} from "../../contracts/v2/access/PredgeAccess.sol"; +import {PullCredit} from "../../contracts/v2/access/PullCredit.sol"; + +abstract contract ArbiterBase is Test { + address internal safe = makeAddr("safe"); + address internal op = makeAddr("operator"); + address internal payer = makeAddr("payer"); + address internal payee = makeAddr("payee"); + address internal stranger = makeAddr("stranger"); + + uint64 internal constant WINDOW = 1 days; + uint256 internal constant MIN_BOND = 1e15; + uint16 internal constant BPS = 500; // 5% of the payment + + PredgeRefundArbiterV2 internal arb; + MockRefundProtocolV2 internal rp; + + bytes32 internal constant EV_PAYER = keccak256("payer evidence"); + bytes32 internal constant EV_PAYEE = keccak256("payee evidence"); + bytes32 internal constant EV_COUNTER = keccak256("counter evidence"); + + function _deployArbiter() internal { + vm.warp(1_760_000_000); + // The mock needs the arbiter address and vice versa: predict the arbiter (next CREATE after the mock). + uint256 n = vm.getNonce(address(this)); + address predicted = vm.computeCreateAddress(address(this), n + 1); + rp = new MockRefundProtocolV2(predicted); + arb = new PredgeRefundArbiterV2(safe, op, address(rp), WINDOW, MIN_BOND, BPS); + assertEq(address(arb), predicted); + vm.deal(op, 1_000 ether); + vm.deal(payer, 10 ether); + vm.deal(payee, 10 ether); + } + + function _pay(uint256 amount) internal returns (uint256 pid) { + pid = rp.recordPayment(payer, payee, amount, block.timestamp + 30 days); + } + + function _file(uint256 pid) internal { + vm.prank(payer); + arb.fileEvidence(pid, EV_PAYER); + vm.prank(payee); + arb.fileEvidence(pid, EV_PAYEE); + } + + function _rule(uint256 pid, address from, bytes32 ev, PredgeRefundArbiterV2.Direction d, uint256 value) internal { + vm.prank(op); + arb.rule{value: value}(pid, from, ev, d); + } + + function _status(uint256 pid) internal view returns (PredgeRefundArbiterV2.Status st) { + st = arb.rulings(pid).status; + } +} + +contract PredgeRefundArbiterV2Test is ArbiterBase { + PredgeRefundArbiterV2.Direction internal constant REFUND = PredgeRefundArbiterV2.Direction.Refund; + PredgeRefundArbiterV2.Direction internal constant RELEASE = PredgeRefundArbiterV2.Direction.Release; + PredgeRefundArbiterV2.Direction internal constant NONE = PredgeRefundArbiterV2.Direction.None; + + event EvidenceFiled(uint256 indexed paymentID, address indexed party, bytes32 evidenceHash); + event Ruled(uint256 indexed paymentID, address indexed staker, address indexed evidenceFrom, bytes32 evidenceHash, PredgeRefundArbiterV2.Direction direction, uint256 bond, uint64 windowEnd); + event Disputed(uint256 indexed paymentID, address indexed disputer, bytes32 counterEvidence, uint64 expiresAt); + event Finalized(uint256 indexed paymentID, address indexed staker, PredgeRefundArbiterV2.Direction direction, uint256 bond); + event Overturned(uint256 indexed paymentID, address indexed harmed, PredgeRefundArbiterV2.Direction finalDirection, uint256 bond); + event Voided(uint256 indexed paymentID, address indexed staker, uint256 bond); + event ParamsUpdated(uint64 challengeWindow, uint256 minBond, uint16 bondBps); + + function setUp() public { + _deployArbiter(); + } + + /// @dev `new` under `vm.expectRevert` ends the test early in forge; deploy through an external call instead. + function mk(address o, address p, address r, uint64 w, uint256 m, uint16 b) external returns (address) { + return address(new PredgeRefundArbiterV2(o, p, r, w, m, b)); + } + + function test_constructor_and_params() public { + assertEq(arb.owner(), safe); + assertEq(arb.operator(), op); + assertEq(address(arb.refundProtocol()), address(rp)); + vm.expectRevert(Owned2Step.ZeroAddress.selector); + this.mk(safe, op, address(0), WINDOW, MIN_BOND, BPS); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + this.mk(safe, op, address(rp), 1 minutes, MIN_BOND, BPS); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + this.mk(safe, op, address(rp), 31 days, MIN_BOND, BPS); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + this.mk(safe, op, address(rp), WINDOW, 0, BPS); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + this.mk(safe, op, address(rp), WINDOW, MIN_BOND, 10_001); + + vm.prank(op); + vm.expectRevert(Owned2Step.NotOwner.selector); + arb.setParams(2 days, 1, 1); + vm.startPrank(safe); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + arb.setParams(1 minutes, 5, 1000); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + arb.setParams(2 days, 5, 10_001); + vm.stopPrank(); + vm.prank(safe); + vm.expectEmit(false, false, false, true); + emit ParamsUpdated(2 days, 5, 1000); + arb.setParams(2 days, 5, 1000); + assertEq(arb.requiredBond(1 ether), 0.1 ether); + assertEq(arb.requiredBond(1), 5); + } + + event LockupForwarded(address indexed recipient, uint256 lockupSeconds); + + function test_setLockupSeconds_isOwnerOnlyPassThrough() public { + uint256 ok = WINDOW + arb.RESOLUTION_TIMEOUT() + 1; + vm.prank(op); + vm.expectRevert(Owned2Step.NotOwner.selector); + arb.setLockupSeconds(payee, ok); + vm.expectEmit(true, false, false, true); + emit LockupForwarded(payee, ok); + vm.prank(safe); + arb.setLockupSeconds(payee, ok); + assertEq(rp.lockupSeconds(payee), ok); + } + + /// @dev Olympix 5.3.12: the forwarded lockup is bounded below by window + resolution timeout and above by MAX_LOCKUP. + function test_setLockupSeconds_bounded() public { + uint256 floor_ = uint256(WINDOW) + 14 days; // literal: also pins RESOLUTION_TIMEOUT + vm.startPrank(safe); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + arb.setLockupSeconds(payee, 0); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + arb.setLockupSeconds(payee, floor_); + arb.setLockupSeconds(payee, floor_ + 1); + arb.setLockupSeconds(payee, 365 days); + assertEq(arb.MAX_LOCKUP(), 365 days); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + arb.setLockupSeconds(payee, 365 days + 1); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + arb.setLockupSeconds(payee, type(uint256).max); + // the floor follows the current challenge window + arb.setParams(2 days, MIN_BOND, BPS); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + arb.setLockupSeconds(payee, floor_ + 1); + arb.setLockupSeconds(payee, floor_ + 1 days + 1); + vm.stopPrank(); + assertEq(rp.lockupSeconds(payee), floor_ + 1 days + 1); + } + + // ---- evidence ---- + + function test_fileEvidence_onlyPartiesWriteOnce() public { + uint256 pid = _pay(1 ether); + vm.expectEmit(true, true, false, true); + emit EvidenceFiled(pid, payer, EV_PAYER); + vm.prank(payer); + arb.fileEvidence(pid, EV_PAYER); + assertEq(arb.filedEvidence(pid, payer), EV_PAYER); + + vm.prank(payer); + vm.expectRevert(abi.encodeWithSelector(PredgeRefundArbiterV2.EvidenceAlreadyFiled.selector, pid, payer)); + arb.fileEvidence(pid, EV_COUNTER); + vm.prank(stranger); + vm.expectRevert(PredgeRefundArbiterV2.NotAParty.selector); + arb.fileEvidence(pid, EV_COUNTER); + vm.prank(op); + vm.expectRevert(PredgeRefundArbiterV2.NotAParty.selector); // the arbiter cannot manufacture the record + arb.fileEvidence(pid, EV_COUNTER); + vm.prank(payee); + vm.expectRevert(PredgeRefundArbiterV2.ZeroHash.selector); + arb.fileEvidence(pid, bytes32(0)); + vm.prank(payee); + vm.expectRevert(PredgeRefundArbiterV2.NotAParty.selector); // payment that does not exist: no party + arb.fileEvidence(999, EV_PAYEE); + } + + function test_fileEvidence_closedOnceRuled() public { + uint256 pid = _pay(1 ether); + vm.prank(payer); + arb.fileEvidence(pid, EV_PAYER); + _rule(pid, payer, EV_PAYER, REFUND, 1 ether); + vm.prank(payee); + vm.expectRevert(PredgeRefundArbiterV2.EvidenceClosed.selector); + arb.fileEvidence(pid, EV_PAYEE); + } + + // ---- rule ---- + + function test_rule_recordsAndExecutesNothing() public { + uint256 pid = _pay(1 ether); + _file(pid); + uint256 bondNeeded = arb.requiredBond(1 ether); // 5% = 0.05 ether + assertEq(bondNeeded, 0.05 ether); + vm.expectEmit(true, true, true, true); + emit Ruled(pid, op, payer, EV_PAYER, REFUND, bondNeeded, uint64(block.timestamp) + WINDOW); + _rule(pid, payer, EV_PAYER, REFUND, bondNeeded); + assertEq(rp.refundCount(), 0, "optimistic: nothing executes inside rule()"); + assertEq(uint8(_status(pid)), uint8(PredgeRefundArbiterV2.Status.Ruled)); + assertEq(arb.totalBonded(), bondNeeded); + assertEq(arb.rulingCount(), 1); + assertEq(arb.harmedParty(pid), payee); + PredgeRefundArbiterV2.Ruling memory r = arb.rulings(pid); + assertEq(r.evidenceFrom, payer); + assertEq(r.evidenceHash, EV_PAYER); + assertEq(r.staker, op); + assertEq(r.payer, payer); + assertEq(r.payee, payee); + assertEq(r.bond, bondNeeded); + assertEq(r.window, WINDOW); + assertEq(r.ruledAt, block.timestamp); + assertEq(r.expiresAt, block.timestamp + WINDOW + arb.RESOLUTION_TIMEOUT()); + assertEq(uint8(r.direction), uint8(REFUND)); + } + + function test_recordsAreComplete_afterEachTerminalPath() public { + // dispute + overturn + uint256 p1 = _pay(1 ether); + _file(p1); + _rule(p1, payer, EV_PAYER, REFUND, 1 ether); + vm.prank(payee); + arb.dispute(p1, EV_COUNTER); + assertEq(arb.rulings(p1).counterEvidence, EV_COUNTER); + vm.prank(safe); + arb.resolve(p1, RELEASE); + assertEq(uint8(arb.rulings(p1).status), uint8(PredgeRefundArbiterV2.Status.Overturned)); + assertEq(arb.rulings(p1).bond, 0); + // finalize + uint256 p2 = _pay(1 ether); + _file(p2); + _rule(p2, payer, EV_PAYER, RELEASE, 1 ether); + // expire + uint256 p3 = _pay(1 ether); + _file(p3); + _rule(p3, payee, EV_PAYEE, RELEASE, 1 ether); + vm.warp(block.timestamp + WINDOW); + arb.finalize(p2); + assertEq(arb.rulings(p2).bond, 0); + assertEq(uint8(arb.rulings(p2).status), uint8(PredgeRefundArbiterV2.Status.Finalized)); + vm.warp(block.timestamp + arb.RESOLUTION_TIMEOUT()); + arb.expire(p3); + assertEq(arb.rulings(p3).bond, 0); + assertEq(uint8(arb.rulings(p3).status), uint8(PredgeRefundArbiterV2.Status.Voided)); + // a ruling that is upheld after a dispute also zeroes the bond + uint256 p4 = _pay(1 ether); + _file(p4); + _rule(p4, payer, EV_PAYER, REFUND, 1 ether); + vm.prank(payee); + arb.dispute(p4, EV_COUNTER); + vm.prank(safe); + arb.resolve(p4, REFUND); + assertEq(arb.rulings(p4).bond, 0); + } + + function test_rule_reverts() public { + uint256 pid = _pay(1 ether); + _file(pid); + uint256 need = arb.requiredBond(1 ether); + vm.deal(stranger, 1 ether); + vm.prank(stranger); + vm.expectRevert(Operated.NotOperator.selector); + arb.rule{value: need}(pid, payer, EV_PAYER, REFUND); + + vm.startPrank(op); + vm.expectRevert(PredgeRefundArbiterV2.ZeroHash.selector); + arb.rule{value: need}(pid, payer, bytes32(0), REFUND); + vm.expectRevert(PredgeRefundArbiterV2.BadDirection.selector); + arb.rule{value: need}(pid, payer, EV_PAYER, NONE); + vm.expectRevert(PredgeRefundArbiterV2.UnknownPayment.selector); + arb.rule{value: need}(777, payer, EV_PAYER, REFUND); + vm.expectRevert(PredgeRefundArbiterV2.NotAParty.selector); // evidence from someone who is not a party + arb.rule{value: need}(pid, stranger, EV_PAYER, REFUND); + vm.expectRevert(abi.encodeWithSelector(PredgeRefundArbiterV2.NoEvidenceFiled.selector, pid, payer)); + arb.rule{value: need}(pid, payer, EV_PAYEE, REFUND); // cites a hash the payer never filed + vm.expectRevert(abi.encodeWithSelector(PredgeRefundArbiterV2.BondTooSmall.selector, need)); + arb.rule{value: need - 1}(pid, payer, EV_PAYER, REFUND); + arb.rule{value: need}(pid, payer, EV_PAYER, REFUND); + vm.expectRevert(abi.encodeWithSelector(PredgeRefundArbiterV2.AlreadyRuled.selector, pid)); + arb.rule{value: need}(pid, payer, EV_PAYER, REFUND); + vm.stopPrank(); + } + + function test_rule_operatorCannotBeAParty() public { + uint256 pid = rp.recordPayment(op, payee, 1 ether, block.timestamp + 30 days); + vm.prank(op); + arb.fileEvidence(pid, EV_PAYER); // the operator is the payer: it may file as a party... + vm.prank(op); + vm.expectRevert(PredgeRefundArbiterV2.OperatorIsParty.selector); // ...but cannot then rule on its own payment + arb.rule{value: 1 ether}(pid, op, EV_PAYER, REFUND); + uint256 pid2 = rp.recordPayment(payer, op, 1 ether, block.timestamp + 30 days); + vm.prank(op); + vm.expectRevert(PredgeRefundArbiterV2.OperatorIsParty.selector); + arb.rule{value: 1 ether}(pid2, payer, EV_PAYER, REFUND); + } + + /// @dev Olympix 5.2.1: the whole ruling (window + full resolution period) must end before release, for + /// BOTH directions (a Release can be overturned into a Refund). Boundary is exclusive. + function test_rule_needsLockupBeyondWindowAndResolution() public { + uint256 edge = block.timestamp + WINDOW + 14 days; // literal 14 days pins RESOLUTION_TIMEOUT + uint256 pid = rp.recordPayment(payer, payee, 1 ether, edge); + _file(pid); + vm.startPrank(op); + vm.expectRevert(PredgeRefundArbiterV2.LockupTooShort.selector); + arb.rule{value: 1 ether}(pid, payer, EV_PAYER, REFUND); + vm.expectRevert(PredgeRefundArbiterV2.LockupTooShort.selector); + arb.rule{value: 1 ether}(pid, payee, EV_PAYEE, RELEASE); + vm.stopPrank(); + uint256 pid2 = rp.recordPayment(payer, payee, 1 ether, edge + 1); + _file(pid2); + _rule(pid2, payer, EV_PAYER, REFUND, 1 ether); + uint256 pid3 = rp.recordPayment(payer, payee, 1 ether, edge + 1); + _file(pid3); + _rule(pid3, payee, EV_PAYEE, RELEASE, 1 ether); + assertEq(arb.RESOLUTION_TIMEOUT(), 14 days); + } + + function test_bondScalesWithPayment() public { + uint256 pid = _pay(1000 ether); + _file(pid); + assertEq(arb.requiredBond(1000 ether), 50 ether); + vm.prank(op); + vm.expectRevert(abi.encodeWithSelector(PredgeRefundArbiterV2.BondTooSmall.selector, 50 ether)); + arb.rule{value: MIN_BOND}(pid, payer, EV_PAYER, REFUND); + } + + // ---- undisputed path ---- + + function test_finalize_refund_afterWindow() public { + uint256 pid = _pay(1 ether); + _file(pid); + uint256 need = arb.requiredBond(1 ether); + _rule(pid, payer, EV_PAYER, REFUND, need); + vm.expectRevert(PredgeRefundArbiterV2.WindowOpen.selector); + arb.finalize(pid); + vm.warp(block.timestamp + WINDOW); + vm.expectEmit(true, true, false, true); + emit Finalized(pid, op, REFUND, need); + vm.prank(stranger); + arb.finalize(pid); + assertEq(rp.refundCount(), 1); + assertEq(arb.credit(op), need); + assertEq(arb.totalBonded(), 0); + vm.expectRevert(PredgeRefundArbiterV2.BadStatus.selector); + arb.finalize(pid); + } + + function test_finalize_release_executesNothing() public { + uint256 pid = _pay(1 ether); + _file(pid); + _rule(pid, payee, EV_PAYEE, RELEASE, MIN_BOND + 1 ether); + vm.warp(block.timestamp + WINDOW); + arb.finalize(pid); + assertEq(rp.refundCount(), 0); + assertEq(arb.credit(op), MIN_BOND + 1 ether); + } + + function test_finalize_revertsIfRefundProtocolReverts_thenExpireVoids() public { + uint256 pid = _pay(1 ether); + _file(pid); + uint256 need = arb.requiredBond(1 ether); + _rule(pid, payer, EV_PAYER, REFUND, need); + vm.warp(block.timestamp + WINDOW); + vm.mockCallRevert(address(rp), abi.encodeWithSelector(MockRefundProtocolV2.refundByArbiter.selector, pid), "rp down"); + vm.expectRevert("rp down"); + arb.finalize(pid); + vm.expectRevert(PredgeRefundArbiterV2.NotExpired.selector); + arb.expire(pid); + vm.warp(block.timestamp + arb.RESOLUTION_TIMEOUT()); + vm.expectEmit(true, true, false, true); + emit Voided(pid, op, need); + arb.expire(pid); + assertEq(arb.credit(op), need, "bond is never stuck"); + assertEq(rp.refundCount(), 0, "voided: nothing executed"); + assertEq(uint8(_status(pid)), uint8(PredgeRefundArbiterV2.Status.Voided)); + } + + // ---- dispute path (H-03) ---- + + function test_dispute_freezesAndOwnerOverturnsRefund() public { + uint256 pid = _pay(1 ether); + _file(pid); + uint256 need = arb.requiredBond(1 ether); + _rule(pid, payer, EV_PAYER, REFUND, need); // harms the payee + vm.expectEmit(true, true, false, true); + emit Disputed(pid, payee, EV_COUNTER, uint64(block.timestamp) + arb.RESOLUTION_TIMEOUT()); + vm.prank(payee); + arb.dispute(pid, EV_COUNTER); + assertEq(uint8(_status(pid)), uint8(PredgeRefundArbiterV2.Status.Disputed)); + + vm.warp(block.timestamp + WINDOW); + vm.expectRevert(PredgeRefundArbiterV2.BadStatus.selector); // frozen: cannot finalize + arb.finalize(pid); + + vm.expectEmit(true, true, false, true); + emit Overturned(pid, payee, RELEASE, need); + vm.prank(safe); + arb.resolve(pid, RELEASE); + assertEq(arb.credit(payee), need, "operator's bond goes to the harmed party"); + assertEq(arb.credit(op), 0); + assertEq(rp.refundCount(), 0); + assertEq(arb.overturnCount(), 1); + assertEq(arb.totalBonded(), 0); + } + + function test_dispute_ownerUpholds() public { + uint256 pid = _pay(1 ether); + _file(pid); + uint256 need = arb.requiredBond(1 ether); + _rule(pid, payer, EV_PAYER, REFUND, need); + vm.prank(payee); + arb.dispute(pid, EV_COUNTER); + vm.prank(safe); + arb.resolve(pid, REFUND); + assertEq(arb.credit(op), need); + assertEq(rp.refundCount(), 1); + assertEq(uint8(_status(pid)), uint8(PredgeRefundArbiterV2.Status.Finalized)); + } + + function test_dispute_ownerOverturnsReleaseIntoRefund() public { + uint256 pid = _pay(1 ether); + _file(pid); + uint256 need = arb.requiredBond(1 ether); + _rule(pid, payee, EV_PAYEE, RELEASE, need); // harms the payer + assertEq(arb.harmedParty(pid), payer); + vm.prank(payer); + arb.dispute(pid, EV_COUNTER); + vm.prank(safe); + arb.resolve(pid, REFUND); + assertEq(arb.credit(payer), need); + assertEq(rp.refundCount(), 1); + } + + function test_dispute_reverts() public { + uint256 pid = _pay(1 ether); + _file(pid); + vm.prank(payee); + vm.expectRevert(PredgeRefundArbiterV2.ZeroHash.selector); + arb.dispute(pid, bytes32(0)); + vm.prank(payee); + vm.expectRevert(PredgeRefundArbiterV2.BadStatus.selector); // not ruled yet + arb.dispute(pid, EV_COUNTER); + _rule(pid, payer, EV_PAYER, REFUND, MIN_BOND + 1 ether); + vm.prank(payer); // the party the ruling FAVOURS cannot freeze it + vm.expectRevert(PredgeRefundArbiterV2.NotHarmedParty.selector); + arb.dispute(pid, EV_COUNTER); + vm.prank(stranger); + vm.expectRevert(PredgeRefundArbiterV2.NotHarmedParty.selector); + arb.dispute(pid, EV_COUNTER); + vm.warp(block.timestamp + WINDOW); + vm.prank(payee); + vm.expectRevert(PredgeRefundArbiterV2.WindowClosed.selector); + arb.dispute(pid, EV_COUNTER); + } + + function test_resolve_reverts() public { + uint256 pid = _pay(1 ether); + _file(pid); + _rule(pid, payer, EV_PAYER, REFUND, MIN_BOND + 1 ether); + vm.startPrank(safe); + vm.expectRevert(PredgeRefundArbiterV2.BadDirection.selector); + arb.resolve(pid, NONE); + vm.expectRevert(PredgeRefundArbiterV2.BadStatus.selector); // not disputed + arb.resolve(pid, REFUND); + vm.stopPrank(); + vm.prank(payee); + arb.dispute(pid, EV_COUNTER); + vm.prank(op); + vm.expectRevert(Owned2Step.NotOwner.selector); + arb.resolve(pid, RELEASE); + vm.warp(block.timestamp + arb.RESOLUTION_TIMEOUT()); + vm.prank(safe); + vm.expectRevert(PredgeRefundArbiterV2.ResolutionExpired.selector); + arb.resolve(pid, RELEASE); + arb.expire(pid); // an idle owner cannot freeze the bond forever + assertEq(arb.credit(op), MIN_BOND + 1 ether); + } + + function test_expire_badStatus() public { + vm.expectRevert(PredgeRefundArbiterV2.BadStatus.selector); + arb.expire(1); + } + + function test_harmedParty_unruledIsZero() public view { + assertEq(arb.harmedParty(5), address(0)); + } + + function test_paramChangeIsNotRetroactive() public { + uint256 pid = _pay(1 ether); + _file(pid); + _rule(pid, payer, EV_PAYER, REFUND, MIN_BOND + 1 ether); + vm.prank(safe); + arb.setParams(30 days, 1e18, 10_000); // owner tries to stretch the window / raise the bond afterwards + vm.warp(block.timestamp + WINDOW); + arb.finalize(pid); // still the old 1 day window + assertEq(arb.credit(op), MIN_BOND + 1 ether); + } + + function test_bondWithdraw() public { + uint256 pid = _pay(1 ether); + _file(pid); + _rule(pid, payer, EV_PAYER, RELEASE, 2 ether); + vm.warp(block.timestamp + WINDOW); + arb.finalize(pid); + uint256 b = op.balance; + vm.prank(op); + arb.withdraw(); + assertEq(op.balance, b + 2 ether); + vm.prank(op); + vm.expectRevert(PullCredit.NothingToWithdraw.selector); + arb.withdraw(); + } + + function testFuzz_bondAlwaysAtLeastScaledAndFloor(uint128 amount, uint16 bps, uint128 floor_) public { + bps = uint16(bound(bps, 0, 10_000)); + floor_ = uint128(bound(floor_, 1, 1e24)); + vm.prank(safe); + arb.setParams(WINDOW, floor_, bps); + uint256 need = arb.requiredBond(amount); + assertGe(need, floor_); + assertGe(need * 10_000, uint256(amount) * bps); + assertLe(need, uint256(floor_) + uint256(amount)); // never more than floor + amount at bps <= 100% + } +} diff --git a/test/foundry-v2/PredgeValidatorBondV2.t.sol b/test/foundry-v2/PredgeValidatorBondV2.t.sol new file mode 100644 index 0000000..e575e90 --- /dev/null +++ b/test/foundry-v2/PredgeValidatorBondV2.t.sol @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Base, NoReceive} from "./Base.sol"; +import {PredgeValidatorBondV2} from "../../contracts/v2/PredgeValidatorBondV2.sol"; +import {IAgentJobV2} from "../../contracts/v2/interfaces/IAgentJobV2.sol"; +import {PullCredit} from "../../contracts/v2/access/PullCredit.sol"; +import {Owned2Step, Operated} from "../../contracts/v2/access/PredgeAccess.sol"; + +contract PredgeValidatorBondV2Test is Base { + bytes32 internal EXPECTED; // sha256(GOOD), set in setUp (sha256 is not a compile-time constant) + bytes32 internal constant SALT = keccak256("salt"); + bytes32 internal constant OTHER = keccak256("something else"); + + event Committed(bytes32 indexed requestHash, uint256 indexed jobId, address indexed staker, bytes32 commitment, uint256 bond); + event Scored(bytes32 indexed requestHash, uint256 indexed jobId, uint8 score, bytes32 expected, address staker, uint256 bond); + event Released(bytes32 indexed requestHash, address indexed staker, uint256 bond); + event Forfeited(bytes32 indexed requestHash, address indexed provider, uint256 bond); + event ParamsUpdated(uint256 minBond, uint16 bondBps); + + function setUp() public { + _deployJobAndBond(); + EXPECTED = _good(); + } + + // ---- construction / params (M-01, L-01) ---- + + /// @dev `new` under `vm.expectRevert` ends the test early in forge; deploy through an external call instead. + function mk(address o, address p, address j, uint256 m, uint16 b) external returns (address) { + return address(new PredgeValidatorBondV2(o, p, j, m, b)); + } + + function test_constructor() public view { + assertEq(bond.owner(), safe); + assertEq(bond.operator(), op); + assertEq(address(bond.job()), address(job)); + assertEq(bond.minBond(), MIN_BOND); + assertEq(bond.bondBps(), BPS); + } + + function test_constructor_reverts() public { + vm.expectRevert(Owned2Step.ZeroAddress.selector); + this.mk(address(0), op, address(job), MIN_BOND, BPS); + vm.expectRevert(Owned2Step.ZeroAddress.selector); + this.mk(safe, address(0), address(job), MIN_BOND, BPS); + vm.expectRevert(Owned2Step.ZeroAddress.selector); + this.mk(safe, op, address(0), MIN_BOND, BPS); + vm.expectRevert(PredgeValidatorBondV2.BadParams.selector); + this.mk(safe, op, address(job), 0, BPS); + vm.expectRevert(PredgeValidatorBondV2.BadParams.selector); + this.mk(safe, op, address(job), MIN_BOND, 10_001); + this.mk(safe, op, address(job), 1, 10_000); // both bounds inclusive + } + + function test_setParams_ownerOnlyAndBounded() public { + vm.prank(op); + vm.expectRevert(Owned2Step.NotOwner.selector); + bond.setParams(1, 5000); + vm.startPrank(safe); + vm.expectRevert(PredgeValidatorBondV2.BadParams.selector); + bond.setParams(0, 5000); + vm.expectRevert(PredgeValidatorBondV2.BadParams.selector); + bond.setParams(1, 10_001); + vm.expectEmit(false, false, false, true); + emit ParamsUpdated(7, 5000); + bond.setParams(7, 5000); + vm.stopPrank(); + assertEq(bond.minBond(), 7); + assertEq(bond.bondBps(), 5000); + } + + function test_paramChangeIsNotRetroactive() public { + (uint256 id, bytes32 spec) = _newJob(10 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 10 ether); + vm.prank(safe); + bond.setParams(MIN_BOND, 0); // owner drops the bps afterwards + _deliver(id, GOOD); + _score(spec, 100, EXPECTED, SALT); + assertEq(bond.credit(op), 10 ether, "the staker gets back exactly what it posted"); + } + + function test_requiredBond_isMaxOfFloorAndScaled() public { + assertEq(bond.requiredBond(1), MIN_BOND); + assertEq(bond.requiredBond(10 ether), 10 ether); + vm.prank(safe); + bond.setParams(MIN_BOND, 2500); + assertEq(bond.requiredBond(10 ether), 2.5 ether); + assertEq(bond.requiredBond(3), MIN_BOND); + vm.prank(safe); + bond.setParams(1, 3333); + assertEq(bond.requiredBond(10), 4, "rounds up: ceil(3.333)"); + } + + // ---- stakeAndCommit ---- + + function test_stake_ok() public { + (uint256 id, bytes32 spec) = _newJob(2 ether, EXPECTED, SALT); + vm.expectEmit(true, true, true, true); + emit Committed(spec, id, op, _commitment(EXPECTED, SALT), 2 ether); + _stake(spec, EXPECTED, SALT, id, 2 ether); + (address staker, PredgeValidatorBondV2.Status st,, uint64 stakedAt,, uint256 jobId, uint256 b, bytes32 c, bytes32 e) = bond.stakes(spec); + assertEq(staker, op); + assertEq(uint8(st), uint8(PredgeValidatorBondV2.Status.Committed)); + assertEq(stakedAt, block.timestamp); + assertEq(jobId, id); + assertEq(b, 2 ether); + assertEq(c, _commitment(EXPECTED, SALT)); + assertEq(e, bytes32(0), "expected is hidden until the reveal"); + assertEq(bond.totalBonded(), 2 ether); + } + + function test_stake_reverts() public { + (uint256 id, bytes32 spec) = _newJob(2 ether, EXPECTED, SALT); + bytes32 c = _commitment(EXPECTED, SALT); + vm.prank(stranger); + vm.expectRevert(Operated.NotOperator.selector); + bond.stakeAndCommit{value: 2 ether}(spec, c, id); + + vm.startPrank(op); + vm.expectRevert(PredgeValidatorBondV2.ZeroHash.selector); + bond.stakeAndCommit{value: 2 ether}(bytes32(0), c, id); + vm.expectRevert(PredgeValidatorBondV2.ZeroHash.selector); + bond.stakeAndCommit{value: 2 ether}(spec, bytes32(0), id); + vm.expectRevert(abi.encodeWithSelector(PredgeValidatorBondV2.BondTooSmall.selector, 2 ether)); + bond.stakeAndCommit{value: 2 ether - 1}(spec, c, id); + vm.expectRevert(PredgeValidatorBondV2.NotOpen.selector); + bond.stakeAndCommit{value: 2 ether}(spec, c, 999); // unknown job + // Olympix 5.1.1: the operator cannot commit to a value the client did not fix in the specHash + vm.expectRevert(PredgeValidatorBondV2.CommitmentNotBound.selector); + bond.stakeAndCommit{value: 2 ether}(spec, _commitment(OTHER, SALT), id); + vm.expectRevert(PredgeValidatorBondV2.CommitmentNotBound.selector); + bond.stakeAndCommit{value: 2 ether}(keccak256("other spec"), c, id); + // a correctly derived hash for ANOTHER commitment is still not this job's spec + bytes32 otherSpec = _bspec(OTHER, SALT); + vm.expectRevert(PredgeValidatorBondV2.JobNotBound.selector); + bond.stakeAndCommit{value: 2 ether}(otherSpec, _commitment(OTHER, SALT), id); + bond.stakeAndCommit{value: 2 ether}(spec, c, id); + vm.expectRevert(PredgeValidatorBondV2.AlreadyCommitted.selector); + bond.stakeAndCommit{value: 2 ether}(spec, c, id); + vm.stopPrank(); + } + + function test_stake_wrongEvaluatorNotBound() public { + bytes32 spec = _bspec(EXPECTED, SALT); + vm.prank(client); + uint256 id = job.createJob{value: 1 ether}(provider, stranger, spec, uint64(block.timestamp + 1 days)); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.JobNotBound.selector); + bond.stakeAndCommit{value: 1 ether}(spec, _commitment(EXPECTED, SALT), id); + } + + function test_stake_operatorCannotBeProvider() public { + bytes32 spec = _bspec(EXPECTED, SALT); + vm.prank(client); + uint256 id = job.createJob{value: 1 ether}(op, address(bond), spec, uint64(block.timestamp + 1 days)); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.ProviderIsOperator.selector); + bond.stakeAndCommit{value: 1 ether}(spec, _commitment(EXPECTED, SALT), id); + } + + /// @dev Olympix 5.2.3: the operator cannot be the client of a job it stakes on or scores. + function test_stake_operatorCannotBeClient() public { + bytes32 spec = _bspec(EXPECTED, SALT); + vm.prank(op); + uint256 id = job.createJob{value: 1 ether}(provider, address(bond), spec, uint64(block.timestamp + 1 days)); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.ClientIsOperator.selector); + bond.stakeAndCommit{value: 1 ether}(spec, _commitment(EXPECTED, SALT), id); + } + + function test_score_rotatedOperatorCannotBeClient() public { + address newOp = makeAddr("newOp"); + vm.deal(newOp, 10 ether); + bytes32 spec = _bspec(EXPECTED, SALT); + vm.prank(newOp); + uint256 id = job.createJob{value: 1 ether}(provider, address(bond), spec, uint64(block.timestamp + 1 days)); + _stake(spec, EXPECTED, SALT, id, 1 ether); // old operator stakes: fine, it is not the client + _deliver(id, GOOD); + vm.prank(safe); + bond.setOperator(newOp); // the owner rotates to the job's client + vm.prank(newOp); + vm.expectRevert(PredgeValidatorBondV2.ClientIsOperator.selector); + bond.recordScore(spec, 100, EXPECTED, SALT, 0); + } + + function test_stake_notOpenOnceSubmittedOrLapsed() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, EXPECTED, SALT); + _submit(id, OTHER); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.NotOpen.selector); + bond.stakeAndCommit{value: 1 ether}(spec, _commitment(EXPECTED, SALT), id); + + bytes32 salt2 = keccak256("salt2"); + (uint256 id2, bytes32 spec2) = _newJob(1 ether, EXPECTED, salt2); + vm.warp(block.timestamp + 7 days); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.NotOpen.selector); + bond.stakeAndCommit{value: 1 ether}(spec2, _commitment(EXPECTED, salt2), id2); + } + + // ---- recordScore ---- + + function test_score_pass_movesTheMoneyAndReturnsTheBond() public { + (uint256 id, bytes32 spec) = _newJob(4 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 4 ether); + _deliver(id, GOOD); + assertEq(bond.verdictFor(id, EXPECTED), 100); + vm.expectEmit(true, true, false, true); + emit Scored(spec, id, 100, EXPECTED, op, 4 ether); + _score(spec, 100, EXPECTED, SALT); + assertEq(uint8(job.getJob(id).state), uint8(IAgentJobV2.State.Completed)); + assertEq(job.credit(provider), 4 ether); + assertEq(job.getJob(id).reason, keccak256("reason")); + (, PredgeValidatorBondV2.Status st, uint8 sc,, uint64 scoredAt,, uint256 left,, bytes32 revealed) = bond.stakes(spec); + assertEq(uint8(st), uint8(PredgeValidatorBondV2.Status.Scored)); + assertEq(sc, 100); + assertEq(revealed, EXPECTED); + assertEq(left, 0, "a scored stake holds no bond"); + assertEq(scoredAt, block.timestamp); + assertGe(scoredAt, job.getJob(id).submittedAt, "the verdict never precedes the submission"); + assertEq(bond.credit(op), 4 ether, "bond credited back to the staker with the verdict"); + assertEq(bond.totalBonded(), 0); + } + + function test_score_fail_refundsClient() public { + (uint256 id, bytes32 spec) = _newJob(4 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 4 ether); + _submit(id, OTHER); + assertEq(bond.verdictFor(id, EXPECTED), 0); + _score(spec, 0, EXPECTED, SALT); + assertEq(uint8(job.getJob(id).state), uint8(IAgentJobV2.State.Rejected)); + assertEq(job.credit(client), 4 ether); + assertEq(bond.credit(op), 4 ether); + } + + function test_score_reverts() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, EXPECTED, SALT); + vm.startPrank(op); + vm.expectRevert(PredgeValidatorBondV2.BadScore.selector); + bond.recordScore(spec, 50, EXPECTED, SALT, 0); + vm.expectRevert(PredgeValidatorBondV2.BadScore.selector); + bond.recordScore(spec, 1, EXPECTED, SALT, 0); + vm.expectRevert(PredgeValidatorBondV2.NotCommitted.selector); + bond.recordScore(spec, 100, EXPECTED, SALT, 0); + bond.stakeAndCommit{value: 1 ether}(spec, _commitment(EXPECTED, SALT), id); + vm.expectRevert(PredgeValidatorBondV2.BadReveal.selector); + bond.recordScore(spec, 100, OTHER, SALT, 0); + vm.expectRevert(PredgeValidatorBondV2.BadReveal.selector); + bond.recordScore(spec, 100, EXPECTED, OTHER, 0); + vm.expectRevert(PredgeValidatorBondV2.NotSubmitted.selector); + bond.recordScore(spec, 100, EXPECTED, SALT, 0); + vm.stopPrank(); + _deliver(id, GOOD); + vm.prank(stranger); + vm.expectRevert(Operated.NotOperator.selector); + bond.recordScore(spec, 100, EXPECTED, SALT, 0); + _score(spec, 100, EXPECTED, SALT); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.BadStatus.selector); + bond.recordScore(spec, 100, EXPECTED, SALT, 0); + } + + function test_score_afterEvaluationWindowRevertsAtTheJob() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 1 ether); + _deliver(id, GOOD); + vm.warp(block.timestamp + EVAL_WINDOW); + vm.prank(op); + vm.expectRevert(); // job.complete -> PastDeadline: a verdict after the deadline is impossible + bond.recordScore(spec, 100, EXPECTED, SALT, 0); + } + + // ---- option (a): a false verdict cannot be recorded ---- + + function _expectContradiction(bytes32 spec, uint8 score, bytes32 salt) internal { + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.VerdictContradictsEvidence.selector); + bond.recordScore(spec, score, EXPECTED, salt, 0); + } + + function test_falsePass_wrongBytes_reverts() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 1 ether); + _deliver(id, "not the output"); + _expectContradiction(spec, 100, SALT); + _score(spec, 0, EXPECTED, SALT); // the true verdict goes through + assertEq(job.credit(client), 1 ether); + } + + /// @dev The provider knows `expected` (e.g. leaked by the client) and claims it without bytes: still not a delivery. + function test_falsePass_claimedHashWithoutBytes_reverts() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 1 ether); + _submit(id, EXPECTED); + assertEq(bond.verdictFor(id, EXPECTED), 0); + _expectContradiction(spec, 100, SALT); + _score(spec, 0, EXPECTED, SALT); + assertEq(job.credit(provider), 0); + } + + function test_falseFail_correctBytes_reverts() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 1 ether); + _deliver(id, GOOD); + _expectContradiction(spec, 0, SALT); + _score(spec, 100, EXPECTED, SALT); + assertEq(job.credit(provider), 1 ether); + } + + /// @dev bondBps = 0 (the shipped value) and the smallest floor: the false pass is still impossible (Olympix 5.1.2). + function test_falsePass_impossibleAtZeroBps() public { + vm.prank(safe); + bond.setParams(1, 0); + (uint256 id, bytes32 spec) = _newJob(100 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 1); + _deliver(id, "colluding provider, wrong output"); + _expectContradiction(spec, 100, SALT); + assertEq(uint8(job.getJob(id).state), uint8(IAgentJobV2.State.Submitted)); + assertEq(job.credit(provider), 0); + } + + // ---- release ---- + + function test_release_whenJobExpiredWithoutVerdict() public { + (uint256 id, bytes32 spec) = _newJob(2 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 2 ether); + vm.expectRevert(PredgeValidatorBondV2.JobNotExpired.selector); + bond.release(spec); + vm.warp(block.timestamp + 7 days); + vm.expectRevert(PredgeValidatorBondV2.JobNotExpired.selector); // lapsed but not yet claimed + bond.release(spec); + job.claimRefund(id); + address newOp = makeAddr("newOp"); + vm.prank(safe); + bond.setOperator(newOp); + vm.expectEmit(true, true, false, true); + emit Released(spec, op, 2 ether); + vm.prank(newOp); // the new key triggers it, the OLD staker is credited (L-04) + bond.release(spec); + assertEq(bond.credit(op), 2 ether); + assertEq(bond.credit(newOp), 0); + (, PredgeValidatorBondV2.Status st,,,,, uint256 left,,) = bond.stakes(spec); + assertEq(uint8(st), uint8(PredgeValidatorBondV2.Status.Released)); + assertEq(left, 0); + vm.expectRevert(PredgeValidatorBondV2.BadStatus.selector); + bond.release(spec); + } + + function test_release_afterSilentEvaluationWindow() public { + (uint256 id, bytes32 spec) = _newJob(2 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 2 ether); + _deliver(id, GOOD); + vm.warp(block.timestamp + EVAL_WINDOW); + job.claimRefund(id); + vm.expectEmit(true, true, false, true); + emit Forfeited(spec, provider, 2 ether); + bond.release(spec); + // Olympix 5.2.2: silence on delivered work forfeits the bond to the provider + assertEq(bond.credit(op), 0); + assertEq(bond.credit(provider), 2 ether); + assertEq(bond.totalBonded(), 0); + (, PredgeValidatorBondV2.Status st,,,,, uint256 left,,) = bond.stakes(spec); + assertEq(uint8(st), uint8(PredgeValidatorBondV2.Status.Forfeited)); + assertEq(left, 0); + assertEq(job.credit(client), 2 ether, "documented timeout policy: the escrow still goes to the client"); + vm.expectRevert(PredgeValidatorBondV2.BadStatus.selector); + bond.release(spec); + } + + function test_release_reverts() public { + vm.expectRevert(PredgeValidatorBondV2.NotCommitted.selector); + bond.release(keccak256("none")); + (uint256 id, bytes32 spec) = _newJob(1 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 1 ether); + _deliver(id, GOOD); + _score(spec, 100, EXPECTED, SALT); + vm.expectRevert(PredgeValidatorBondV2.BadStatus.selector); + bond.release(spec); + } + + // ---- pull payments ---- + + function test_payoutToRevertingProviderDoesNotBlock() public { + // The forfeit beneficiary cannot receive value: the bond still lands as credit and can be redirected. + NoReceive bad = new NoReceive(); + bytes32 spec = _bspec(EXPECTED, SALT); + vm.prank(client); + uint256 id = job.createJob{value: 1 ether}(address(bad), address(bond), spec, uint64(block.timestamp + 1 days)); + _stake(spec, EXPECTED, SALT, id, 1 ether); + vm.prank(address(bad)); + job.submit(id, OTHER, ""); + vm.warp(block.timestamp + EVAL_WINDOW); + job.claimRefund(id); + bond.release(spec); + assertEq(bond.credit(address(bad)), 1 ether); + vm.expectRevert(PullCredit.TransferFailed.selector); + bad.callWithdraw(address(bond)); + vm.prank(address(bad)); + bond.withdrawTo(payable(stranger)); + assertEq(bond.credit(address(bad)), 0); + assertEq(stranger.balance, 10 ether + 1 ether); + } + + // ---- fuzz ---- + + /// @dev Whatever the provider submits and whatever the operator claims, recordScore accepts the verdict + /// iff it equals the truth ("delivered" = bytes hashed on-chain whose hash equals the client's + /// expected value). A false verdict always reverts and moves nothing. + function testFuzz_noFalseVerdictCanBeRecorded(bytes32 salt, bool viaBytes, bool matching, bool claimPass, bytes32 junk, uint96 escrow) + public + { + vm.assume(junk != bytes32(0) && junk != EXPECTED); + escrow = uint96(bound(escrow, 1, 100 ether)); + (uint256 id, bytes32 spec) = _newJob(escrow, EXPECTED, salt); + uint256 b = bond.requiredBond(escrow); + _stake(spec, EXPECTED, salt, id, b); + if (viaBytes) _deliver(id, matching ? GOOD : abi.encode(junk)); + else _submit(id, matching ? EXPECTED : junk); // a claimed hash, even the right one, is not a delivery + bool truth = viaBytes && matching; + assertEq(bond.verdictFor(id, EXPECTED), truth ? 100 : 0); + uint8 claimed = claimPass ? 100 : 0; + if (claimPass != truth) { + _expectContradiction(spec, claimed, salt); + assertEq(uint8(job.getJob(id).state), uint8(IAgentJobV2.State.Submitted), "a false verdict moves nothing"); + assertEq(bond.totalBonded(), b); + } else { + _score(spec, claimed, EXPECTED, salt); + assertEq(uint8(job.getJob(id).state), uint8(truth ? IAgentJobV2.State.Completed : IAgentJobV2.State.Rejected)); + assertEq(job.credit(truth ? provider : client), escrow); + assertEq(bond.credit(op), b); + } + } + + /// @dev Any output bytes at all: a pass is recorded iff sha256(bytes) == expected, a fail iff not. + function testFuzz_passIffOnChainHashMatches(bytes calldata out, bool claimPass) public { + vm.assume(out.length != 0); + (uint256 id, bytes32 spec) = _newJob(1 ether, EXPECTED, SALT); + _stake(spec, EXPECTED, SALT, id, 1 ether); + _deliver(id, out); + bool truth = sha256(out) == EXPECTED; + if (claimPass != truth) _expectContradiction(spec, claimPass ? 100 : 0, SALT); + else _score(spec, claimPass ? 100 : 0, EXPECTED, SALT); + } +} diff --git a/test/foundry-v2/V1Findings.t.sol b/test/foundry-v2/V1Findings.t.sol new file mode 100644 index 0000000..319d87e --- /dev/null +++ b/test/foundry-v2/V1Findings.t.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Base, NoReceive} from "./Base.sol"; +import {ArbiterBase} from "./PredgeRefundArbiterV2.t.sol"; +import {PredgeValidatorBondV2} from "../../contracts/v2/PredgeValidatorBondV2.sol"; +import {PredgeRefundArbiterV2} from "../../contracts/v2/PredgeRefundArbiterV2.sol"; +import {PredgeAgentValidatorV2} from "../../contracts/v2/PredgeAgentValidatorV2.sol"; +import {PredgeSettlementV2} from "../../contracts/v2/PredgeSettlementV2.sol"; +import {AgentJobV2} from "../../contracts/v2/AgentJobV2.sol"; +import {IAgentJobV2} from "../../contracts/v2/interfaces/IAgentJobV2.sol"; +import {Owned2Step} from "../../contracts/v2/access/PredgeAccess.sol"; +import {PullCredit} from "../../contracts/v2/access/PullCredit.sol"; + +/// @notice v2 versions of the v1 test_BUG_ / test_finding_ tests (PR #2): each v1 attack must revert or be harmless. +contract V1FindingsBondTest is Base { + bytes32 internal E; // sha256(GOOD) + bytes32 constant SALT = keccak256("salt"); + bytes32 constant X = keccak256("other"); + + function setUp() public { + _deployJobAndBond(); + E = _good(); + } + + // VB-01 / test_BUG_stake_allowedAfterDeliverableIsPublic, test_finding_postHocCommitMakesFalsePassUnslashable + function test_fixed_VB01_stakeAfterDeliverableReverts() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, E, SALT); + _submit(id, X); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.NotOpen.selector); + bond.stakeAndCommit{value: 1 ether}(spec, keccak256(abi.encode(E, SALT)), id); + } + + // test_finding_scoreBeforeDeliveryThenReclaim + function test_fixed_scoreBeforeDeliveryThenReclaim() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, E, SALT); + _stake(spec, E, SALT, id, 1 ether); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.NotSubmitted.selector); + bond.recordScore(spec, 100, E, SALT, 0); + } + + // test_BUG_*_truncates*: amounts are uint256 end to end; a value above 2^96 is stored exactly. + function test_fixed_noUint96Truncation() public { + uint256 big = uint256(type(uint96).max) + 1 ether; + vm.deal(client, big); + bytes32 spec = _bspec(E, SALT); + vm.prank(client); + uint256 id = job.createJob{value: big}(provider, address(bond), spec, uint64(block.timestamp + 1 days)); + assertEq(job.getJob(id).escrow, big); + assertEq(address(job).balance, big); + vm.deal(op, big); + _stake(spec, E, SALT, id, big); + (,,,,,, uint256 b,,) = bond.stakes(spec); + assertEq(b, big); + assertEq(bond.totalBonded(), big); + } + + // test_finding_rotationInheritsStakesAndBypassesProviderCheck (VB-04) + function test_fixed_VB04_rotatedOperatorCannotScoreItsOwnJob() public { + bytes32 spec = _bspec(E, SALT); + address newOp = makeAddr("newOp"); + vm.prank(client); + uint256 id = job.createJob{value: 1 ether}(newOp, address(bond), spec, uint64(block.timestamp + 1 days)); + _stake(spec, E, SALT, id, 1 ether); // old op stakes: fine, the provider is not op + vm.prank(newOp); + job.submit(id, X, ""); + vm.prank(safe); + bond.setOperator(newOp); // owner rotates to the provider + vm.prank(newOp); + vm.expectRevert(PredgeValidatorBondV2.ProviderIsOperator.selector); + bond.recordScore(spec, 100, E, SALT, 0); + } + + // test_finding_operatorRotationRedirectsBond (L-04): the bond always goes back to the staker. + function test_fixed_rotationDoesNotRedirectBond() public { + (uint256 id, bytes32 spec) = _newJob(1 ether, E, SALT); + _stake(spec, E, SALT, id, 1 ether); + _deliver(id, GOOD); + address newOp = makeAddr("newOp"); + vm.prank(safe); + bond.setOperator(newOp); + vm.prank(newOp); // the new key scores (it was handed the preimage); the OLD staker is credited + bond.recordScore(spec, 100, E, SALT, 0); + assertEq(bond.credit(newOp), 0); + assertEq(bond.credit(op), 1 ether, "the bond came back to the staker with the verdict"); + } + + // test_finding_comparisonIsRawEquality (VB-05): raw equality of the stored 32 bytes, and only for a hash + // the chain computed. After Olympix 5.1.1 a claimed hash with no bytes never counts as delivered. + function test_fixed_VB05_rawEqualityOfStoredHash() public { + bytes memory out = "output"; + bytes32 h = sha256(out); + (uint256 id, bytes32 spec) = _newJob(1 ether, h, SALT); + _stake(spec, h, SALT, id, 1 ether); + _deliver(id, out); + _score(spec, 100, h, SALT); + // a claimed hash equal to `expected`, with no bytes, is NOT a delivery: passing it is refused + bytes32 salt2 = keccak256("salt2"); + (uint256 id2, bytes32 spec2) = _newJob(1 ether, E, salt2); + _stake(spec2, E, salt2, id2, 1 ether); + _submit(id2, E); + vm.prank(op); + vm.expectRevert(PredgeValidatorBondV2.VerdictContradictsEvidence.selector); + bond.recordScore(spec2, 100, E, salt2, 0); + } + + // Parameter boundaries are inclusive. + function test_paramBoundaries() public { + vm.startPrank(safe); + bond.setParams(1, 0); + bond.setParams(1, 10_000); + vm.expectRevert(PredgeValidatorBondV2.BadParams.selector); + bond.setParams(1, 10_001); + vm.expectRevert(PredgeValidatorBondV2.BadParams.selector); + bond.setParams(0, 0); + vm.stopPrank(); + } + + function test_requiredBond_floorEqualsScaled() public { + assertEq(bond.requiredBond(MIN_BOND), MIN_BOND); + } +} + +contract V1FindingsArbiterTest is ArbiterBase { + function setUp() public { _deployArbiter(); } + + // test_finding_operatorCanCiteSockPuppetEvidence / test_finding_formerOperatorCanFile + function test_fixed_sockPuppetEvidence() public { + uint256 pid = _pay(1 ether); + address sock = makeAddr("sock"); + vm.prank(sock); + vm.expectRevert(PredgeRefundArbiterV2.NotAParty.selector); + arb.fileEvidence(pid, EV_PAYER); + vm.prank(op); + vm.expectRevert(PredgeRefundArbiterV2.NotAParty.selector); // the (former) operator is no party either + arb.fileEvidence(pid, EV_PAYER); + } + + // test_BUG_rule_truncatesBondAbove2pow96 + function test_fixed_noUint96Truncation() public { + uint256 big = uint256(type(uint96).max) + 1 ether; + vm.deal(op, big); + uint256 pid = rp.recordPayment(payer, payee, 1 ether, block.timestamp + 30 days); + _file(pid); + _rule(pid, payer, EV_PAYER, PredgeRefundArbiterV2.Direction.Release, big); + assertEq(arb.rulings(pid).bond, big); + assertEq(arb.totalBonded(), big); + } + + // RA-04 / test_finding_setParamsIsRetroactive and RA-07 window overflow + function test_fixed_RA04_RA07_paramsBoundedAndNotRetroactive() public { + uint256 pid = _pay(1 ether); + _file(pid); + _rule(pid, payer, EV_PAYER, PredgeRefundArbiterV2.Direction.Release, 1 ether); + vm.prank(safe); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + arb.setParams(type(uint64).max, 1, 0); // no overflow / no bricked reclaim + vm.prank(safe); + arb.setParams(30 days, 1, 0); + vm.warp(block.timestamp + WINDOW); + arb.finalize(pid); + } + + function test_paramBoundaries() public { + vm.startPrank(safe); + arb.setParams(1 hours, 1, 0); + arb.setParams(30 days, 1, 10_000); + vm.expectRevert(PredgeRefundArbiterV2.BadParams.selector); + arb.setParams(30 days + 1, 1, 0); + vm.stopPrank(); + } + + function test_requiredBond_floorEqualsScaled() public view { + assertEq(arb.requiredBond(MIN_BOND * 10_000 / BPS), MIN_BOND); + } + + function test_expireKeepsAccounting() public { + uint256 pid = _pay(1 ether); + _file(pid); + _rule(pid, payer, EV_PAYER, PredgeRefundArbiterV2.Direction.Release, 2 ether); + _rule2(); + vm.warp(block.timestamp + WINDOW + arb.RESOLUTION_TIMEOUT()); + arb.expire(pid); + assertEq(arb.totalBonded(), 3 ether, "only the other ruling's bond is still held"); + } + + function _rule2() internal { + uint256 pid = _pay(1 ether); + _file(pid); + _rule(pid, payer, EV_PAYER, PredgeRefundArbiterV2.Direction.Release, 3 ether); + } +} + +contract V1FindingsMiscTest is Base { + function setUp() public { _deployJobAndBond(); } + + // AV-01 / test_BUG_request_frontRunSquatsRequestHash + function test_fixed_AV01_squattingImpossible() public { + PredgeAgentValidatorV2 reg = new PredgeAgentValidatorV2(safe, op, address(0)); + vm.prank(stranger); + vm.expectRevert(PredgeAgentValidatorV2.NotAuthorized.selector); + reg.validationRequest(op, 1, "squat", keccak256("spec")); + // and the hash is domain separated, so a squat on another chain/registry cannot collide anyway + assertTrue(reg.requestHashFor(1, keccak256("spec")) != keccak256("spec")); + } + + // AV-02 / test_BUG_commitLeadTime_zeroWhenSameBlock: documented, not a bug: zero is possible in the same block. + function test_fixed_AV02_sameBlockLeadIsZeroAndDocumented() public { + PredgeAgentValidatorV2 reg = new PredgeAgentValidatorV2(safe, op, address(0)); + vm.startPrank(op); + bytes32 h = reg.validationRequest(op, 1, "x", keccak256("d")); + reg.validationResponse(h, 100, "", keccak256("r"), ""); + vm.stopPrank(); + assertEq(reg.commitLeadTime(h), 0); + } + + // AJ-02 / test_finding_escrowLockedWhenNeitherPartyCanReceive: with pull payments an unreceivable party + // cannot block settlement and can redirect its credit. + function test_fixed_AJ02_unreceivableParties() public { + NoReceive a = new NoReceive(); + NoReceive b = new NoReceive(); + vm.deal(address(a), 1 ether); + bytes32 spec = _spec(1); + vm.prank(address(a)); + uint256 id = job.createJob{value: 1 ether}(address(b), op, spec, uint64(block.timestamp + 1 days)); + vm.prank(address(b)); + job.submit(id, keccak256("d"), ""); + vm.prank(op); + job.reject(id, 0, ""); + assertEq(job.credit(address(a)), 1 ether); + vm.prank(address(a)); + job.withdrawTo(payable(stranger)); + assertEq(address(job).balance, 0); + } + + // test_finding_openJobHasNoTimeoutRefund + function test_fixed_openJobTimeoutRefund() public { + (uint256 id,) = _newJob(1, 1 ether); + vm.warp(block.timestamp + 7 days); + job.claimRefund(id); + assertEq(job.credit(client), 1 ether); + } + + // PS-01 / test_finding_withdrawToZeroBurns + function test_fixed_PS01_withdrawToZeroReverts() public { + PredgeSettlementV2 s = new PredgeSettlementV2(safe); + vm.deal(address(s), 1 ether); + vm.prank(safe); + vm.expectRevert(Owned2Step.ZeroAddress.selector); + s.withdraw(payable(address(0))); + } +} diff --git a/test/plan-v2.test.mjs b/test/plan-v2.test.mjs new file mode 100644 index 0000000..9b08887 --- /dev/null +++ b/test/plan-v2.test.mjs @@ -0,0 +1,78 @@ +// v2 plan: deterministic addresses, role wiring, config safety, pinned compiler. No network. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { getCreateAddress, Wallet } from "ethers"; +import { resolvePlan } from "../lib/plan.mjs"; +import { SET_V2, CHAINS } from "../lib/chains.mjs"; +import { chainParams, realRoles, v2KeyFile, ANVIL_KEYS, forkRoles } from "../lib/v2.mjs"; + +const D = "0x1111111111111111111111111111111111111111"; // sample only +const OWNER = "0x2222222222222222222222222222222222222222"; +const OP = "0x3333333333333333333333333333333333333333"; +const plan = (chain = "base", d = D) => resolvePlan(d, { owner: OWNER, operator: OP, ...chainParams(chain) }, SET_V2); + +test("addresses depend only on deployer and nonce order, not on the chain or chain params", () => { + const a = plan("base"), b = plan("arc"); + assert.deepEqual(a.map((s) => s.address), b.map((s) => s.address)); + a.forEach((s, i) => assert.equal(s.address, getCreateAddress({ from: D, nonce: (SET_V2.nonceBase ?? 0) + i }))); + a.forEach((s, i) => assert.equal(s.nonce, (SET_V2.nonceBase ?? 0) + i)); + assert.notDeepEqual(a.map((s) => s.initcode), b.map((s) => s.initcode), "bond floors differ per chain, so initcode does"); +}); + +test("v2.1 continues the 20.10 deployer at nonce 6 (nonces 0..5 hold the v2.0 set on Arc and Base)", () => { + assert.equal(SET_V2.nonceBase, 6); + const live = JSON.parse(readFileSync(new URL("../deployments/v2/arc.json", import.meta.url), "utf8")); + const p = resolvePlan(live.deployer, { owner: OWNER, operator: OP, ...chainParams("arc") }, SET_V2); + const old = new Set(Object.values(live.contracts).map((c) => c.address)); + for (const s of p) assert.ok(!old.has(s.address), `${s.name} would collide with a v2.0 address`); +}); + +test("the plan order is fixed: append-only", () => { + assert.deepEqual(SET_V2.plan.map((s) => s.name), ["PredgeAgentValidatorV2", "AgentJobV2", "PredgeValidatorBondV2", "PredgeSettlementV2", "MockRefundProtocolV2", "PredgeRefundArbiterV2"]); +}); + +test("roles are wired from the constructor args and the deployer holds none", () => { + const by = Object.fromEntries(plan().map((s) => [s.name, s])); + assert.equal(by.PredgeAgentValidatorV2.args[0], OWNER); + assert.equal(by.PredgeAgentValidatorV2.args[1], OP); + assert.equal(by.PredgeValidatorBondV2.args[0], OWNER); + assert.equal(by.PredgeValidatorBondV2.args[1], OP); + assert.equal(by.PredgeValidatorBondV2.args[2], by.AgentJobV2.address); + assert.equal(by.PredgeSettlementV2.args[0], OWNER); + assert.equal(by.MockRefundProtocolV2.args[0], by.PredgeRefundArbiterV2.address); + assert.equal(by.PredgeRefundArbiterV2.args[2], by.MockRefundProtocolV2.address); + for (const s of Object.values(by)) assert.ok(!s.args.includes(D), `${s.name} must not take the deployer as an argument`); +}); + +test("every chain has a non-zero bond floor, and the live config refuses to run while unset (or is complete)", () => { + for (const name of Object.keys(CHAINS)) { + const p = chainParams(name); + assert.ok(BigInt(p.minBond) > 0n && BigInt(p.arbiterMinBond) > 0n, name); + } + // Since 20.10 the owner has filled config/v2.config.json; before that realRoles() had to refuse to run. + let roles = null; + try { roles = realRoles(); } catch (e) { assert.match(e.message, /is not set/); } + if (roles) assert.equal(new Set(Object.values(roles)).size, 3, "deployer, owner and operator are distinct"); +}); + +test("v2 key handling: never the v1 key dir; fork accounts are not accepted as real roles", () => { + assert.throws(() => v2KeyFile(undefined), /--key-file/); + assert.throws(() => v2KeyFile("/Users/x/.predge-deployer/deployer.json"), /refusing/); + assert.equal(v2KeyFile("/tmp/new-v2.json"), "/tmp/new-v2.json"); + const f = forkRoles(); + assert.equal(new Set(Object.values(f)).size, 3); + assert.equal(new Wallet(ANVIL_KEYS[1]).address, f.owner); +}); + +test("every v2 source pins solc 0.8.26 exactly and has no floating pragma", () => { + const walk = (dir) => readdirSync(dir).flatMap((n) => { const p = `${dir}/${n}`; return statSync(p).isDirectory() ? walk(p) : p.endsWith(".sol") ? [p] : []; }); + const files = walk(new URL("../contracts/v2", import.meta.url).pathname); + assert.ok(files.length >= 8); + for (const f of files) assert.match(readFileSync(f, "utf8"), /^pragma solidity 0\.8\.26;$/m, f); +}); + +test("v2 contracts carry no selfdestruct, delegatecall, tx.origin or inline assembly", () => { + const files = SET_V2.plan.map((s) => readFileSync(new URL(`../contracts/${s.file}.sol`, import.meta.url), "utf8")); + for (const src of files) assert.doesNotMatch(src.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""), /selfdestruct|delegatecall|tx\.origin|assembly/); +});