diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index 8e455ec88..8995d1e7b 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -76,6 +76,34 @@ jobs: - name: Ensure gas snapshots have been updated. TODO Fix this. Snapshot checks are currently disabled as they don't match in CI. run: FORGE_SNAPSHOT_CHECK=false forge test --isolate + legacy-bytecode-parity: + runs-on: ubuntu-latest + needs: contracts-changed + if: needs.contracts-changed.outputs.src == 'true' || + needs.contracts-changed.outputs.foundry == 'true' || + needs.contracts-changed.outputs.test == 'true' || + needs.contracts-changed.outputs.scripts == 'true' || + needs.contracts-changed.outputs.ci == 'true' + steps: + - name: checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: ${{ env.FOUNDRY_VERSION }} + + - name: forge build + run: forge build --silent + + - name: Verify legacy/ bytecode matches deployed OLD impl + run: python3 contracts/scripts/verify-legacy-bytecode.py + + - name: Verify storage layout interop between src/ and src/legacy/ + run: python3 contracts/scripts/verify-storage-layout.py + upgradability: runs-on: ubuntu-latest needs: contracts-changed @@ -301,6 +329,10 @@ jobs: DEPLOYER_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" ADMIN_ADDRESS: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + # Non-functional stub: the new ABI must never route through the legacy + # fallback, so DeploymentTest runs against a dead delegate target. Any + # accidental legacy-path call hits this address and fails to return. + BOUNDLESS_LEGACY_IMPL: "0xdededededededededededededededededededede" - name: forge test after new deployment env: @@ -320,6 +352,9 @@ jobs: DEPLOYER_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" ADMIN_ADDRESS: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + # Keep the same stub as the deploy step so the upgraded impl also points + # its legacy fallback at a dead target (overrides the in-market default). + BOUNDLESS_LEGACY_IMPL: "0xdededededededededededededededededededede" - name: forge test after upgrade env: diff --git a/contracts/deployment-test/Deploymnet.t.sol b/contracts/deployment-test/Deploymnet.t.sol index 91be6af0e..51df5741e 100644 --- a/contracts/deployment-test/Deploymnet.t.sol +++ b/contracts/deployment-test/Deploymnet.t.sol @@ -12,6 +12,12 @@ import {IRiscZeroVerifier} from "risc0/IRiscZeroVerifier.sol"; import {IRiscZeroSetVerifier} from "risc0/IRiscZeroSetVerifier.sol"; import {IRiscZeroSelectable} from "risc0/IRiscZeroSelectable.sol"; +// The deployment-test exercises the deployed market via its current ABI: +// requests are fulfilled through the new batched fulfill path on the new +// BoundlessMarket, so all struct + interface imports come from +// contracts/src/. The legacy ABI served via the fallback is not exercised +// here; in this deployment the legacy impl is a non-functional stub that +// the new path never invokes. import {IBoundlessMarket} from "../src/IBoundlessMarket.sol"; import {Callback} from "../src/types/Callback.sol"; import {Fulfillment} from "../src/types/Fulfillment.sol"; @@ -103,7 +109,7 @@ contract DeploymentTest is Test { require(keccak256(address(verifier).code) != keccak256(bytes("")), "verifier code is empty"); require(deployment.boundlessRouter != address(0), "no boundless router address is set"); require( - deployment.boundlessRouter == address(BoundlessMarket(address(boundlessMarket)).ROUTER()), + deployment.boundlessRouter == address(BoundlessMarket(payable(address(boundlessMarket))).ROUTER()), "boundless router address does not match boundless market" ); } @@ -122,15 +128,15 @@ contract DeploymentTest is Test { require(address(stakeToken) != address(0), "no collateral token address is set"); require(keccak256(address(stakeToken).code) != keccak256(bytes("")), "collateral token code is empty"); require( - address(stakeToken) == BoundlessMarket(address(boundlessMarket)).COLLATERAL_TOKEN_CONTRACT(), + address(stakeToken) == BoundlessMarket(payable(address(boundlessMarket))).COLLATERAL_TOKEN_CONTRACT(), "collateral token address does not match boundless market" ); } function testBoundlessMarketOwner() external view { require( - BoundlessMarket(address(boundlessMarket)) - .hasRole(BoundlessMarket(address(boundlessMarket)).ADMIN_ROLE(), deployment.admin2), + BoundlessMarket(payable(address(boundlessMarket))) + .hasRole(BoundlessMarket(payable(address(boundlessMarket))).ADMIN_ROLE(), deployment.admin2), "boundless market admin role does not match admin" ); } @@ -191,7 +197,7 @@ contract DeploymentTest is Test { // The market reconstructs and emits the domain-bound request digest from the SlimRequest. bytes32 requestDigest = MessageHashUtils.toTypedDataHash( - BoundlessMarket(address(boundlessMarket)).eip712DomainSeparator(), request.eip712Digest() + BoundlessMarket(payable(address(boundlessMarket))).eip712DomainSeparator(), request.eip712Digest() ); vm.expectEmit(true, true, true, true); diff --git a/contracts/scripts/Deploy.s.sol b/contracts/scripts/Deploy.s.sol index 76799eeac..c95838643 100644 --- a/contracts/scripts/Deploy.s.sol +++ b/contracts/scripts/Deploy.s.sol @@ -19,6 +19,7 @@ import {ControlID} from "../src/blake3-groth16/ControlID.sol"; import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import {ConfigLoader, DeploymentConfig} from "./Config.s.sol"; import {BoundlessMarket} from "../src/BoundlessMarket.sol"; +import {BoundlessMarket as BoundlessMarketLegacy} from "../src/legacy/BoundlessMarketLegacy.sol"; import {HitPoints} from "../src/HitPoints.sol"; import {BoundlessScriptBase} from "./BoundlessScript.s.sol"; @@ -151,9 +152,25 @@ contract Deploy is BoundlessScriptBase, RiscZeroCheats { // var overrides it, e.g. when the router is deployed in the same run). address boundlessRouter = vm.envOr("BOUNDLESS_ROUTER", deploymentConfig.boundlessRouter); require(boundlessRouter != address(0), "boundless router must be set in deployment.toml or BOUNDLESS_ROUTER"); + + // Resolve the legacy impl (delegate-call target for the legacy ABI). + // Production deployments set BOUNDLESS_LEGACY_IMPL to the impl pointed + // to by the proxy before the upgrade (audited bytecode, already on + // chain). When the env var is unset — dev / localnet / fresh networks + // — deploy a fresh legacy impl from contracts/src/legacy/ wired to the + // same verifier and collateral token the new market is about to use. + address legacyImpl = vm.envOr("BOUNDLESS_LEGACY_IMPL", address(0)); + if (legacyImpl == address(0)) { + legacyImpl = address( + new BoundlessMarketLegacy(verifier, applicationVerifier, assessorImageId, bytes32(0), 0, stakeToken) + ); + console2.log("Deployed legacy BoundlessMarket implementation to", legacyImpl); + } else { + console2.log("Using BOUNDLESS_LEGACY_IMPL from env:", legacyImpl); + } bytes32 salt = vm.envOr("SALT", keccak256(abi.encodePacked("salt"))); address newImplementation = - address(new BoundlessMarket{salt: salt}(BoundlessRouter(boundlessRouter), stakeToken)); + address(new BoundlessMarket{salt: salt}(BoundlessRouter(boundlessRouter), stakeToken, legacyImpl)); console2.log("Deployed new BoundlessMarket implementation at", newImplementation); boundlessMarketAddress = address( new ERC1967Proxy{salt: salt}( diff --git a/contracts/scripts/Manage.s.sol b/contracts/scripts/Manage.s.sol index 06e907cb5..0b0d10f7c 100644 --- a/contracts/scripts/Manage.s.sol +++ b/contracts/scripts/Manage.s.sol @@ -10,6 +10,7 @@ import {console2} from "forge-std/console2.sol"; import {Strings} from "openzeppelin/contracts/utils/Strings.sol"; import {IRiscZeroVerifier} from "risc0/IRiscZeroVerifier.sol"; import {BoundlessMarket} from "../src/BoundlessMarket.sol"; +import {BoundlessMarket as BoundlessMarketLegacy} from "../src/legacy/BoundlessMarketLegacy.sol"; import {BoundlessRouter} from "../src/router/BoundlessRouter.sol"; import {BoundlessMarketLib} from "../src/libraries/BoundlessMarketLib.sol"; import {ConfigLoader, DeploymentConfig} from "./Config.s.sol"; @@ -71,12 +72,31 @@ contract DeployBoundlessMarket is BoundlessScriptBase { // deployment config (the BOUNDLESS_ROUTER env var overrides it). address boundlessRouter = vm.envOr("BOUNDLESS_ROUTER", deploymentConfig.boundlessRouter).required("boundless-router"); - + // Resolve the legacy impl (delegate-call target for the legacy ABI). + // Production sets BOUNDLESS_LEGACY_IMPL to the audited on-chain impl; + // when unset (dev / localnet / fresh networks) deploy a fresh one from + // contracts/src/legacy/ wired to the configured verifier and token. + address legacyImpl = vm.envOr("BOUNDLESS_LEGACY_IMPL", address(0)); vm.startBroadcast(getDeployer()); + if (legacyImpl == address(0)) { + legacyImpl = address( + new BoundlessMarketLegacy( + IRiscZeroVerifier(deploymentConfig.verifier), + IRiscZeroVerifier(deploymentConfig.applicationVerifier), + deploymentConfig.assessorImageId, + bytes32(0), + 0, + collateralToken + ) + ); + console2.log("Deployed legacy BoundlessMarket implementation to", legacyImpl); + } else { + console2.log("Using BOUNDLESS_LEGACY_IMPL from env:", legacyImpl); + } // Deploy the proxy contract and initialize the contract bytes32 salt = bytes32(0); address newImplementation = - address(new BoundlessMarket{salt: salt}(BoundlessRouter(boundlessRouter), collateralToken)); + address(new BoundlessMarket{salt: salt}(BoundlessRouter(boundlessRouter), collateralToken, legacyImpl)); address marketAddress = address( new ERC1967Proxy{salt: salt}(newImplementation, abi.encodeCall(BoundlessMarket.initialize, (admin))) ); @@ -84,8 +104,9 @@ contract DeployBoundlessMarket is BoundlessScriptBase { vm.stopBroadcast(); // Verify the deployment - BoundlessMarket market = BoundlessMarket(marketAddress); + BoundlessMarket market = BoundlessMarket(payable(marketAddress)); require(address(market.ROUTER()) == boundlessRouter, "router does not match"); + require(market.LEGACY_IMPL() == legacyImpl, "legacy impl does not match"); require( market.COLLATERAL_TOKEN_CONTRACT() == deploymentConfig.collateralToken, "collateral token does not match" ); @@ -149,8 +170,11 @@ contract UpgradeBoundlessMarket is BoundlessScriptBase { // config (the BOUNDLESS_ROUTER env var overrides it). address boundlessRouter = vm.envOr("BOUNDLESS_ROUTER", deploymentConfig.boundlessRouter).required("boundless-router"); - - BoundlessMarket market = BoundlessMarket(marketAddress); + BoundlessMarket market = BoundlessMarket(payable(marketAddress)); + // Keep the existing delegate-call target by default so the audited + // legacy bytecode the proxy already points at is preserved across the + // upgrade; BOUNDLESS_LEGACY_IMPL can override it to intentionally repoint. + address legacyImpl = vm.envOr("BOUNDLESS_LEGACY_IMPL", market.LEGACY_IMPL()); // Upgrade requires build info from the currently deployed version. // You can get this build info with the following process. @@ -164,7 +188,7 @@ contract UpgradeBoundlessMarket is BoundlessScriptBase { // ``` UpgradeOptions memory opts; opts.constructorData = - BoundlessMarketLib.encodeConstructorArgs(BoundlessRouter(boundlessRouter), collateralToken); + BoundlessMarketLib.encodeConstructorArgs(BoundlessRouter(boundlessRouter), collateralToken, legacyImpl); if (skipSafetyChecks) { console2.log("WARNING: Skipping all upgrade safety checks and reference build!"); @@ -205,7 +229,7 @@ contract UpgradeBoundlessMarket is BoundlessScriptBase { console2.log("Upgraded Boundless Market implementation to: ", newImpl); // Verify the upgrade - BoundlessMarket upgradedMarket = BoundlessMarket(marketAddress); + BoundlessMarket upgradedMarket = BoundlessMarket(payable(marketAddress)); require(address(upgradedMarket.ROUTER()) == boundlessRouter, "upgraded market router does not match"); require( upgradedMarket.COLLATERAL_TOKEN_CONTRACT() == deploymentConfig.collateralToken, @@ -268,7 +292,7 @@ contract RollbackBoundlessMarket is BoundlessScriptBase { vm.stopBroadcast(); // Verify the upgrade - BoundlessMarket upgradedMarket = BoundlessMarket(marketAddress); + BoundlessMarket upgradedMarket = BoundlessMarket(payable(marketAddress)); require( upgradedMarket.COLLATERAL_TOKEN_CONTRACT() == deploymentConfig.collateralToken, "upgraded market stake token does not match" @@ -320,7 +344,7 @@ contract AddBoundlessMarketAdmin is BoundlessScriptBase { require(adminToAdd != address(0), "ADMIN_TO_ADD environment variable not set"); address marketAddress = deploymentConfig.boundlessMarket.required("boundless-market"); - BoundlessMarket market = BoundlessMarket(marketAddress); + BoundlessMarket market = BoundlessMarket(payable(marketAddress)); bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); bytes32 adminRole = market.ADMIN_ROLE(); @@ -388,7 +412,7 @@ contract RemoveBoundlessMarketAdmin is BoundlessScriptBase { require(adminToRemove != address(0), "ADMIN_TO_REMOVE environment variable not set"); address marketAddress = deploymentConfig.boundlessMarket.required("boundless-market"); - BoundlessMarket market = BoundlessMarket(marketAddress); + BoundlessMarket market = BoundlessMarket(payable(marketAddress)); bool gnosisExecute = vm.envOr("GNOSIS_EXECUTE", false); bytes32 adminRole = market.ADMIN_ROLE(); diff --git a/contracts/scripts/verify-legacy-bytecode.py b/contracts/scripts/verify-legacy-bytecode.py new file mode 100755 index 000000000..0317d0b25 --- /dev/null +++ b/contracts/scripts/verify-legacy-bytecode.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Assert that contracts/src/legacy/BoundlessMarketLegacy.sol compiles to +byte-identical bytecode as the OLD BoundlessMarket implementation deployed on +Base mainnet, modulo immutable slots that are baked in at deploy time. + +Two checks: + 1. After masking all positions listed in the artifact's + `immutableReferences`, the legacy artifact and the deployed bytecode are + byte-identical. This proves the legacy source is the audited code. + 2. The value baked into the deployed bytecode at each known immutable's + position matches the expected value declared in + `contracts/test/legacy/deployed-bytecode.meta.toml`. This proves the + deployment was configured with the verifier / assessor / collateral + addresses we expect. + +If this script fails, do not modify it to make the diff smaller — any drift +between the frozen legacy/ source and the deployed audited bytecode is a real +issue. See contracts/src/legacy/LEGACY-FROZEN.md. + +Run via `uv run contracts/scripts/verify-legacy-bytecode.py` from the repo +root, or via `just check-legacy-bytecode`. Requires that `forge build` has +produced the legacy artifact under +`out/BoundlessMarketLegacy.sol/BoundlessMarket.json`. +""" + +import json +import re +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +ARTIFACT = REPO_ROOT / "out" / "BoundlessMarketLegacy.sol" / "BoundlessMarket.json" +DEPLOYED_HEX = REPO_ROOT / "contracts" / "test" / "legacy" / "deployed-bytecode.hex" +META_TOML = REPO_ROOT / "contracts" / "test" / "legacy" / "deployed-bytecode.meta.toml" + + +def strip0x(s: str) -> str: + s = s.strip() + return s[2:] if s.startswith("0x") else s + + +def parse_immutables_section(text: str) -> dict: + """Extract the [immutables] table as {name: raw_value_string}. + + Avoids needing tomllib (Python 3.11+) or a third-party TOML parser so this + works under the system python3 on any CI runner. + """ + in_section = False + out = {} + for raw in text.splitlines(): + line = raw.split("#", 1)[0].strip() # strip comments + whitespace + if not line: + continue + if line.startswith("[") and line.endswith("]"): + in_section = line == "[immutables]" + continue + if not in_section: + continue + m = re.match(r'(\w+)\s*=\s*(.+)$', line) + if not m: + raise ValueError(f"unparseable line in [immutables]: {raw!r}") + key, raw_val = m.group(1), m.group(2).strip() + if raw_val.startswith('"') and raw_val.endswith('"'): + raw_val = raw_val[1:-1] + out[key] = raw_val + return out + + +def normalize_to_32_byte_hex(name: str, value: str) -> str: + """Encode the expected immutable value as a 32-byte lowercase hex string. + + Addresses are 20 bytes left-padded with zeros (right-aligned in the slot). + bytes32 values are 32 bytes as-is. Integer literals are left-padded. + """ + if value.startswith("0x"): + hex_val = value[2:].lower() + if len(hex_val) == 40: # address + return "0" * 24 + hex_val + if len(hex_val) == 64: # bytes32 + return hex_val + raise ValueError(f"{name}: expected 20- or 32-byte hex, got {len(hex_val) // 2} bytes") + # Integer literal — encode big-endian into 32 bytes. + try: + as_int = int(value) + except ValueError as e: + raise ValueError(f"{name}: expected hex or integer, got {value!r}") from e + return f"{as_int:064x}" + + +def map_immutable_name_to_ast_id(artifact: dict) -> dict: + """Walk the contract-level immutable declarations in the artifact's AST.""" + result = {} + for node in artifact["ast"]["nodes"]: + if node.get("nodeType") != "ContractDefinition": + continue + for child in node.get("nodes", []): + if ( + child.get("nodeType") == "VariableDeclaration" + and child.get("mutability") == "immutable" + ): + result[child["name"]] = str(child["id"]) + return result + + +def main() -> int: + if not ARTIFACT.exists(): + print(f"error: {ARTIFACT.relative_to(REPO_ROOT)} not found — run `forge build` first", file=sys.stderr) + return 2 + if not DEPLOYED_HEX.exists(): + print(f"error: {DEPLOYED_HEX.relative_to(REPO_ROOT)} not found", file=sys.stderr) + return 2 + if not META_TOML.exists(): + print(f"error: {META_TOML.relative_to(REPO_ROOT)} not found", file=sys.stderr) + return 2 + + artifact = json.loads(ARTIFACT.read_text()) + legacy = strip0x(artifact["deployedBytecode"]["object"]).lower() + deployed = strip0x(DEPLOYED_HEX.read_text()).lower() + + if len(legacy) != len(deployed): + print(f"bytecode length differs: legacy={len(legacy)} deployed={len(deployed)} (hex chars)", file=sys.stderr) + return 1 + + # --- Check 1: body matches after masking immutables --------------------- + refs = artifact["deployedBytecode"].get("immutableReferences", {}) + legacy_arr = list(legacy) + deployed_arr = list(deployed) + for occurrences in refs.values(): + for occ in occurrences: + start_char = occ["start"] * 2 + end_char = start_char + occ["length"] * 2 + for i in range(start_char, end_char): + legacy_arr[i] = "0" + deployed_arr[i] = "0" + + if legacy_arr != deployed_arr: + for i, (a, b) in enumerate(zip(legacy_arr, deployed_arr)): + if a != b: + ctx_lo = max(0, i - 20) + ctx_hi = i + 40 + print("bytecode differs after masking immutables", file=sys.stderr) + print(f" first diff at hex char {i} (byte {i // 2})", file=sys.stderr) + print(f" legacy: ...{''.join(legacy_arr[ctx_lo:ctx_hi])}...", file=sys.stderr) + print(f" deployed: ...{''.join(deployed_arr[ctx_lo:ctx_hi])}...", file=sys.stderr) + break + return 1 + + # --- Check 2: expected immutable values match what's baked in ----------- + expected_raw = parse_immutables_section(META_TOML.read_text()) + name_to_id = map_immutable_name_to_ast_id(artifact) + + mismatches = [] + checked = [] + for name, raw_value in expected_raw.items(): + ast_id = name_to_id.get(name) + if ast_id is None: + mismatches.append(f"{name}: declared in meta.toml but not found as a contract-level immutable") + continue + positions = refs.get(ast_id) + if not positions: + mismatches.append(f"{name}: no immutableReferences entry for AST id {ast_id}") + continue + try: + expected_hex = normalize_to_32_byte_hex(name, raw_value) + except ValueError as e: + mismatches.append(str(e)) + continue + # All positions for an immutable carry the same baked value; check the first. + start = positions[0]["start"] * 2 + actual_hex = deployed[start : start + 64] + if actual_hex != expected_hex: + mismatches.append( + f"{name}: expected {expected_hex} but deployed has {actual_hex} " + f"(at byte {positions[0]['start']})" + ) + continue + checked.append(name) + + # Surface any immutables referenced in the artifact but not declared in meta — + # likely inherited from a parent contract (e.g. EIP712Upgradeable). Report, + # don't fail, since those values are derived rather than user-supplied. + declared_ids = set(name_to_id.values()) + unchecked_inherited = [k for k in refs if k not in declared_ids] + + if mismatches: + print("immutable value mismatches:", file=sys.stderr) + for m in mismatches: + print(f" - {m}", file=sys.stderr) + return 1 + + bytecode_bytes = len(legacy) // 2 + immutable_positions = sum(len(v) for v in refs.values()) + print(f"OK: legacy/BoundlessMarketLegacy.sol matches deployed OLD impl") + print(f" bytecode size: {bytecode_bytes} bytes") + print(f" immutable positions masked: {immutable_positions}") + print(f" immutable values verified: {', '.join(checked)}") + if unchecked_inherited: + print( + f" inherited immutables not in meta (skipped): " + f"{len(unchecked_inherited)} AST id(s)" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/contracts/scripts/verify-storage-layout.py b/contracts/scripts/verify-storage-layout.py new file mode 100755 index 000000000..754c4953b --- /dev/null +++ b/contracts/scripts/verify-storage-layout.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +"""Assert that the new market and the legacy/ market agree on the storage +slots reachable from both ABIs. + +Delegate-calling the legacy impl from the new market's fallback only works if +every (slot, offset, width) that the legacy code touches means the same thing +in the new market's view of storage. This script enforces that by comparing +the storage layouts emitted by `forge build` (extra_output = storageLayout) +for both `contracts/src/BoundlessMarket.sol:BoundlessMarket` and +`contracts/src/legacy/BoundlessMarketLegacy.sol:BoundlessMarket`. + +Checks: + 1. Top-level storage variables at slot 0, 1, 2 (requestLocks, accounts, + imageUrl) agree on label, slot, offset, and normalized type name. + 2. Every struct type that appears in the top-level storage (Account, + RequestLock, transitively) has identical member layouts in both + artifacts: same labels, same (slot, offset, width). + +Differences in the AST-id suffix of type names (e.g. `RequestId)12840` vs +`RequestId)20065`) are tolerated — only the human-readable struct/enum/ +udvt name matters for delegate-call safety. + +Run via `uv run contracts/scripts/verify-storage-layout.py` from the repo +root, or via `just check-storage-layout`. Requires `forge build` to have run. +""" + +import json +import re +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +NEW_ARTIFACT = REPO_ROOT / "out" / "BoundlessMarket.sol" / "BoundlessMarket.json" +LEGACY_ARTIFACT = REPO_ROOT / "out" / "BoundlessMarketLegacy.sol" / "BoundlessMarket.json" + +# Top-level storage variables shared between the two contracts. Order + +# expected slot are part of the contract — any change here is a real +# divergence that breaks delegate-call interop. +SHARED_TOP_LEVEL = [ + ("requestLocks", 0), + ("accounts", 1), + ("imageUrl", 2), +] + + +# Strip the trailing `_storage` suffix and any embedded AST id sequences from +# a type identifier so two artifacts with different compile-time IDs still +# compare equal when their underlying type names match. +# +# Examples: +# "t_struct(Account)11144_storage" -> "t_struct(Account)_storage" +# "t_mapping(t_userDefinedValueType(RequestId)12840,t_struct(RequestLock)13116_storage)" +# -> "t_mapping(t_userDefinedValueType(RequestId),t_struct(RequestLock)_storage)" +_AST_ID_RE = re.compile(r"\)(\d+)") + + +def normalize_type(t: str) -> str: + return _AST_ID_RE.sub(")", t) + + +def load_layout(artifact_path: Path) -> dict: + if not artifact_path.exists(): + print(f"error: {artifact_path.relative_to(REPO_ROOT)} not found — run `forge build` first", file=sys.stderr) + sys.exit(2) + artifact = json.loads(artifact_path.read_text()) + layout = artifact.get("storageLayout") + if not layout: + print(f"error: {artifact_path.relative_to(REPO_ROOT)} has no storageLayout (extra_output = storageLayout in foundry.toml?)", file=sys.stderr) + sys.exit(2) + return layout + + +def storage_entry(layout: dict, slot: int) -> dict | None: + for entry in layout.get("storage", []): + if int(entry["slot"]) == slot: + return entry + return None + + +def struct_type_keys(layout: dict) -> dict: + """Map normalized struct name -> raw type key in the layout's types map. + + Only includes types that have a `members` field (structs). + """ + out = {} + for key, val in layout.get("types", {}).items(): + if val.get("members") is None: + continue + out[normalize_type(key)] = key + return out + + +def collect_referenced_structs(layout: dict, top_level_labels: list[str]) -> set: + """Walk every type referenced from the named top-level variables and + return the normalized names of struct types in the transitive closure. + + This is the set of structs whose member layouts must match between + artifacts for delegate-call interop to be safe. + """ + types = layout.get("types", {}) + + structs = set() + visited = set() + + def visit(type_key: str) -> None: + if type_key in visited: + return + visited.add(type_key) + node = types.get(type_key) + if node is None: + return + members = node.get("members") + if members is not None: + structs.add(normalize_type(type_key)) + for m in members: + visit(m["type"]) + # mappings / arrays carry their element type info on the type node itself + for child_key in ("base", "key", "value"): + child = node.get(child_key) + if child is not None: + visit(child) + + for entry in layout.get("storage", []): + if entry["label"] in top_level_labels: + visit(entry["type"]) + + return structs + + +def compare_struct_members(name: str, new_members: list, legacy_members: list, errors: list) -> None: + """Assert two member lists describe the same field at the same (slot, offset).""" + if len(new_members) != len(legacy_members): + errors.append( + f"struct {name}: member count differs (new={len(new_members)}, legacy={len(legacy_members)})" + ) + return + for i, (n, l) in enumerate(zip(new_members, legacy_members)): + for field in ("label", "offset", "slot"): + if n[field] != l[field]: + errors.append( + f"struct {name} member #{i}: {field} differs (new={n[field]!r}, legacy={l[field]!r})" + ) + if normalize_type(n["type"]) != normalize_type(l["type"]): + errors.append( + f"struct {name} member #{i} ({n['label']}): type differs " + f"(new={normalize_type(n['type'])!r}, legacy={normalize_type(l['type'])!r})" + ) + + +def main() -> int: + new_layout = load_layout(NEW_ARTIFACT) + legacy_layout = load_layout(LEGACY_ARTIFACT) + + errors: list[str] = [] + + # --- Check 1: shared top-level storage variables ----------------------- + for label, slot in SHARED_TOP_LEVEL: + new_entry = storage_entry(new_layout, slot) + legacy_entry = storage_entry(legacy_layout, slot) + if new_entry is None: + errors.append(f"slot {slot}: missing from new market layout") + continue + if legacy_entry is None: + errors.append(f"slot {slot}: missing from legacy market layout") + continue + for field in ("label", "offset", "slot"): + if new_entry[field] != legacy_entry[field]: + errors.append( + f"slot {slot} ({label}): {field} differs (new={new_entry[field]!r}, legacy={legacy_entry[field]!r})" + ) + if new_entry["label"] != label: + errors.append( + f"slot {slot}: expected label {label!r} in new market, got {new_entry['label']!r}" + ) + if normalize_type(new_entry["type"]) != normalize_type(legacy_entry["type"]): + errors.append( + f"slot {slot} ({label}): type differs after normalization " + f"(new={normalize_type(new_entry['type'])!r}, legacy={normalize_type(legacy_entry['type'])!r})" + ) + + # --- Check 2: transitively reachable struct layouts -------------------- + labels = [lbl for lbl, _ in SHARED_TOP_LEVEL] + new_structs = collect_referenced_structs(new_layout, labels) + legacy_structs = collect_referenced_structs(legacy_layout, labels) + + only_in_new = new_structs - legacy_structs + only_in_legacy = legacy_structs - new_structs + if only_in_new: + errors.append(f"structs referenced by new market but not by legacy: {sorted(only_in_new)}") + if only_in_legacy: + errors.append(f"structs referenced by legacy market but not by new: {sorted(only_in_legacy)}") + + new_struct_keys = struct_type_keys(new_layout) + legacy_struct_keys = struct_type_keys(legacy_layout) + + common = new_structs & legacy_structs + for normalized_name in sorted(common): + new_key = new_struct_keys[normalized_name] + legacy_key = legacy_struct_keys[normalized_name] + new_members = new_layout["types"][new_key]["members"] + legacy_members = legacy_layout["types"][legacy_key]["members"] + compare_struct_members(normalized_name, new_members, legacy_members, errors) + + if errors: + print("storage layout divergence between src/ and src/legacy/:", file=sys.stderr) + for e in errors: + print(f" - {e}", file=sys.stderr) + return 1 + + print("OK: storage layout interop preserved between src/ and src/legacy/") + print(f" shared top-level slots verified: {len(SHARED_TOP_LEVEL)}") + print(f" shared structs verified: {len(common)} ({', '.join(sorted(common))})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/contracts/snapshots/BoundlessMarketBasicTest.json b/contracts/snapshots/BoundlessMarketBasicTest.json index 84abdd04d..04a2fe5ed 100644 --- a/contracts/snapshots/BoundlessMarketBasicTest.json +++ b/contracts/snapshots/BoundlessMarketBasicTest.json @@ -1,45 +1,45 @@ { "ERC20 approve: required for depositCollateral": "45966", - "bytecode size implementation": "21203", + "bytecode size implementation": "22150", "bytecode size proxy": "89", - "deposit: first ever deposit": "50810", - "deposit: second deposit": "33710", - "depositCollateral: 1 HP (tops up market account)": "59271", - "depositCollateral: full (drains testProver account)": "49671", - "depositCollateralWithPermit: 1 HP (tops up market account)": "72236", - "depositCollateralWithPermit: full (drains testProver account)": "72236", - "depositTo: first ever deposit": "50892", - "depositTo: second deposit": "33792", - "fulfill (no journal): a batch of 8": "404072", - "fulfill: a batch of 8": "423989", - "fulfill: a locked request": "111749", - "fulfill: a locked request (locked via prover signature)": "111749", - "fulfill: a locked request with 10kB journal": "366933", - "fulfill: another prover fulfills without payment": "106717", - "fulfill: fulfilled by the locked prover for payment (request already fulfilled by another prover)": "106579", - "fulfillAndWithdraw: a batch of 8": "436402", - "fulfillAndWithdraw: a locked request": "124162", - "lockinRequest: base case": "147304", - "lockinRequest: with prover signature": "156988", - "priceAndFulfill: a single request": "133666", - "priceAndFulfill: a single request (smart contract signature)": "139839", - "priceAndFulfill: a single request (with selector)": "158060", - "priceAndFulfill: a single request that was not locked": "133678", - "priceAndFulfill: a single request that was not locked fulfilled by prover not in allow-list": "133678", - "priceAndFulfill: fulfill already fulfilled was locked request": "129234", - "slash: base case": "100964", - "slash: fulfilled request after lock deadline": "80531", - "submitRequest: with maxPrice ether": "52742", - "submitRequest: without ether": "45899", - "submitRootAndFulfill: a batch of 2 requests": "209326", - "submitRootAndFulfill: a locked request": "155474", - "submitRootAndFulfill: a locked request (locked via prover signature)": "155474", - "submitRootAndFulfillAndWithdraw: a locked request": "166807", - "submitRootAndPriceAndFulfill: a single request": "176097", - "submitRootAndPriceAndFulfill: a single request that was not locked": "176109", - "submitRootAndPriceAndFulfill: a single request that was not locked fulfilled by prover not in allow-list": "176109", - "withdraw: 1 ether": "40287", - "withdraw: full balance": "40299", - "withdrawCollateral: 1 HP balance": "69096", - "withdrawCollateral: full balance": "52092" + "deposit: first ever deposit": "50863", + "deposit: second deposit": "33763", + "depositCollateral: 1 HP (tops up market account)": "59377", + "depositCollateral: full (drains testProver account)": "49777", + "depositCollateralWithPermit: 1 HP (tops up market account)": "72327", + "depositCollateralWithPermit: full (drains testProver account)": "72327", + "depositTo: first ever deposit": "50941", + "depositTo: second deposit": "33841", + "fulfill (no journal): a batch of 8": "416849", + "fulfill: a batch of 8": "436766", + "fulfill: a locked request": "113592", + "fulfill: a locked request (locked via prover signature)": "113592", + "fulfill: a locked request with 10kB journal": "368776", + "fulfill: another prover fulfills without payment": "108361", + "fulfill: fulfilled by the locked prover for payment (request already fulfilled by another prover)": "108207", + "fulfillAndWithdraw: a batch of 8": "449381", + "fulfillAndWithdraw: a locked request": "126207", + "lockinRequest: base case": "149390", + "lockinRequest: with prover signature": "159376", + "priceAndFulfill: a single request": "136268", + "priceAndFulfill: a single request (smart contract signature)": "142444", + "priceAndFulfill: a single request (with selector)": "160662", + "priceAndFulfill: a single request that was not locked": "136280", + "priceAndFulfill: a single request that was not locked fulfilled by prover not in allow-list": "136280", + "priceAndFulfill: fulfill already fulfilled was locked request": "131771", + "slash: base case": "101870", + "slash: fulfilled request after lock deadline": "81277", + "submitRequest: with maxPrice ether": "52895", + "submitRequest: without ether": "46010", + "submitRootAndFulfill: a batch of 2 requests": "212744", + "submitRootAndFulfill: a locked request": "157330", + "submitRootAndFulfill: a locked request (locked via prover signature)": "157330", + "submitRootAndFulfillAndWithdraw: a locked request": "168845", + "submitRootAndPriceAndFulfill: a single request": "178725", + "submitRootAndPriceAndFulfill: a single request that was not locked": "178737", + "submitRootAndPriceAndFulfill: a single request that was not locked fulfilled by prover not in allow-list": "178737", + "withdraw: 1 ether": "40487", + "withdraw: full balance": "40499", + "withdrawCollateral: 1 HP balance": "69309", + "withdrawCollateral: full balance": "52305" } \ No newline at end of file diff --git a/contracts/snapshots/BoundlessMarketBench.json b/contracts/snapshots/BoundlessMarketBench.json index ca3350bed..efcec24cf 100644 --- a/contracts/snapshots/BoundlessMarketBench.json +++ b/contracts/snapshots/BoundlessMarketBench.json @@ -1,22 +1,22 @@ { - "fulfill (with callback): batch of 001": "178726", - "fulfill (with callback): batch of 002": "280151", - "fulfill (with callback): batch of 004": "483950", - "fulfill (with callback): batch of 008": "891238", - "fulfill (with callback): batch of 016": "1545271", - "fulfill (with callback): batch of 032": "2898014", - "fulfill (with selector): batch of 001": "136061", - "fulfill (with selector): batch of 002": "196931", - "fulfill (with selector): batch of 004": "320983", - "fulfill (with selector): batch of 008": "560027", - "fulfill (with selector): batch of 016": "1041560", - "fulfill (with selector): batch of 032": "2042388", - "fulfill: batch of 001": "137035", - "fulfill: batch of 002": "196876", - "fulfill: batch of 004": "318868", - "fulfill: batch of 008": "553751", - "fulfill: batch of 016": "1027027", - "fulfill: batch of 032": "2009899", - "fulfill: batch of 064": "4091379", - "fulfill: batch of 128": "8654662" + "fulfill (with callback): batch of 001": "180884", + "fulfill (with callback): batch of 002": "284186", + "fulfill (with callback): batch of 004": "491739", + "fulfill (with callback): batch of 008": "906535", + "fulfill (with callback): batch of 016": "1575584", + "fulfill (with callback): batch of 032": "2958359", + "fulfill (with selector): batch of 001": "137904", + "fulfill (with selector): batch of 002": "200336", + "fulfill (with selector): batch of 004": "327512", + "fulfill (with selector): batch of 008": "572804", + "fulfill (with selector): batch of 016": "1066833", + "fulfill (with selector): batch of 032": "2092653", + "fulfill: batch of 001": "138878", + "fulfill: batch of 002": "200281", + "fulfill: batch of 004": "325397", + "fulfill: batch of 008": "566528", + "fulfill: batch of 016": "1052300", + "fulfill: batch of 032": "2060164", + "fulfill: batch of 064": "4191628", + "fulfill: batch of 128": "8854879" } \ No newline at end of file diff --git a/contracts/snapshots/BoundlessMarketLegacyBasicTest.json b/contracts/snapshots/BoundlessMarketLegacyBasicTest.json new file mode 100644 index 000000000..7af8887f8 --- /dev/null +++ b/contracts/snapshots/BoundlessMarketLegacyBasicTest.json @@ -0,0 +1,45 @@ +{ + "ERC20 approve: required for depositCollateral": "45966", + "bytecode size implementation": "24371", + "bytecode size proxy": "89", + "deposit: first ever deposit": "50942", + "deposit: second deposit": "33842", + "depositCollateral: 1 HP (tops up market account)": "59403", + "depositCollateral: full (drains testProver account)": "49803", + "depositCollateralWithPermit: 1 HP (tops up market account)": "72277", + "depositCollateralWithPermit: full (drains testProver account)": "72268", + "depositTo: first ever deposit": "51024", + "depositTo: second deposit": "33924", + "fulfill (no journal): a batch of 8": "351707", + "fulfill: a batch of 8": "370252", + "fulfill: a locked request": "87293", + "fulfill: a locked request (locked via prover signature)": "87293", + "fulfill: a locked request with 10kB journal": "344971", + "fulfill: another prover fulfills without payment": "82256", + "fulfill: fulfilled by the locked prover for payment (request already fulfilled by another prover)": "82117", + "fulfillAndWithdraw: a batch of 8": "382122", + "fulfillAndWithdraw: a locked request": "99163", + "lockinRequest: base case": "147046", + "lockinRequest: with prover signature": "156774", + "priceAndFulfill: a single request": "109151", + "priceAndFulfill: a single request (smart contract signature)": "115313", + "priceAndFulfill: a single request (with selector)": "111462", + "priceAndFulfill: a single request that was not locked": "109151", + "priceAndFulfill: a single request that was not locked fulfilled by prover not in allow-list": "109151", + "priceAndFulfill: fulfill already fulfilled was locked request": "107459", + "slash: base case": "101033", + "slash: fulfilled request after lock deadline": "80598", + "submitRequest: with maxPrice ether": "52757", + "submitRequest: without ether": "45914", + "submitRootAndFulfill: a batch of 2 requests": "161173", + "submitRootAndFulfill: a locked request": "121966", + "submitRootAndFulfill: a locked request (locked via prover signature)": "121966", + "submitRootAndFulfillAndWithdraw: a locked request": "133271", + "submitRootAndPriceAndFulfill: a single request": "142381", + "submitRootAndPriceAndFulfill: a single request that was not locked": "142381", + "submitRootAndPriceAndFulfill: a single request that was not locked fulfilled by prover not in allow-list": "142381", + "withdraw: 1 ether": "40358", + "withdraw: full balance": "40370", + "withdrawCollateral: 1 HP balance": "69140", + "withdrawCollateral: full balance": "52136" +} \ No newline at end of file diff --git a/contracts/snapshots/BoundlessMarketLegacyBench.json b/contracts/snapshots/BoundlessMarketLegacyBench.json new file mode 100644 index 000000000..102d7266a --- /dev/null +++ b/contracts/snapshots/BoundlessMarketLegacyBench.json @@ -0,0 +1,22 @@ +{ + "fulfill (with callback): batch of 001": "129170", + "fulfill (with callback): batch of 002": "211957", + "fulfill (with callback): batch of 004": "378227", + "fulfill (with callback): batch of 008": "709522", + "fulfill (with callback): batch of 016": "1208438", + "fulfill (with callback): batch of 032": "2238870", + "fulfill (with selector): batch of 001": "89529", + "fulfill (with selector): batch of 002": "132769", + "fulfill (with selector): batch of 004": "221274", + "fulfill (with selector): batch of 008": "388324", + "fulfill (with selector): batch of 016": "723114", + "fulfill (with selector): batch of 032": "1417729", + "fulfill: batch of 001": "87281", + "fulfill: batch of 002": "128256", + "fulfill: batch of 004": "212234", + "fulfill: batch of 008": "370240", + "fulfill: batch of 016": "686422", + "fulfill: batch of 032": "1343691", + "fulfill: batch of 064": "2722486", + "fulfill: batch of 128": "5673869" +} \ No newline at end of file diff --git a/contracts/snapshots/BoundlessMarketLegacyViaFallbackBasicTest.json b/contracts/snapshots/BoundlessMarketLegacyViaFallbackBasicTest.json new file mode 100644 index 000000000..ba7555ce5 --- /dev/null +++ b/contracts/snapshots/BoundlessMarketLegacyViaFallbackBasicTest.json @@ -0,0 +1,45 @@ +{ + "ERC20 approve: required for depositCollateral": "45966", + "bytecode size implementation": "22150", + "bytecode size proxy": "89", + "deposit: first ever deposit": "50863", + "deposit: second deposit": "33763", + "depositCollateral: 1 HP (tops up market account)": "59377", + "depositCollateral: full (drains testProver account)": "49777", + "depositCollateralWithPermit: 1 HP (tops up market account)": "72327", + "depositCollateralWithPermit: full (drains testProver account)": "72327", + "depositTo: first ever deposit": "50941", + "depositTo: second deposit": "33841", + "fulfill (no journal): a batch of 8": "356440", + "fulfill: a batch of 8": "375414", + "fulfill: a locked request": "91345", + "fulfill: a locked request (locked via prover signature)": "91345", + "fulfill: a locked request with 10kB journal": "351197", + "fulfill: another prover fulfills without payment": "86326", + "fulfill: fulfilled by the locked prover for payment (request already fulfilled by another prover)": "86181", + "fulfillAndWithdraw: a batch of 8": "387284", + "fulfillAndWithdraw: a locked request": "103215", + "lockinRequest: base case": "149390", + "lockinRequest: with prover signature": "159376", + "priceAndFulfill: a single request": "113451", + "priceAndFulfill: a single request (smart contract signature)": "119589", + "priceAndFulfill: a single request (with selector)": "115763", + "priceAndFulfill: a single request that was not locked": "113439", + "priceAndFulfill: a single request that was not locked fulfilled by prover not in allow-list": "113439", + "priceAndFulfill: fulfill already fulfilled was locked request": "111753", + "slash: base case": "101870", + "slash: fulfilled request after lock deadline": "81277", + "submitRequest: with maxPrice ether": "52895", + "submitRequest: without ether": "46010", + "submitRootAndFulfill: a batch of 2 requests": "165436", + "submitRootAndFulfill: a locked request": "126080", + "submitRootAndFulfill: a locked request (locked via prover signature)": "126080", + "submitRootAndFulfillAndWithdraw: a locked request": "137385", + "submitRootAndPriceAndFulfill: a single request": "146719", + "submitRootAndPriceAndFulfill: a single request that was not locked": "146707", + "submitRootAndPriceAndFulfill: a single request that was not locked fulfilled by prover not in allow-list": "146707", + "withdraw: 1 ether": "40487", + "withdraw: full balance": "40499", + "withdrawCollateral: 1 HP balance": "69309", + "withdrawCollateral: full balance": "52305" +} \ No newline at end of file diff --git a/contracts/snapshots/BoundlessMarketLegacyViaFallbackBench.json b/contracts/snapshots/BoundlessMarketLegacyViaFallbackBench.json new file mode 100644 index 000000000..2506c9c94 --- /dev/null +++ b/contracts/snapshots/BoundlessMarketLegacyViaFallbackBench.json @@ -0,0 +1,22 @@ +{ + "fulfill (with callback): batch of 001": "133253", + "fulfill (with callback): batch of 002": "216238", + "fulfill (with callback): batch of 004": "382741", + "fulfill (with callback): batch of 008": "714872", + "fulfill (with callback): batch of 016": "1215510", + "fulfill (with callback): batch of 032": "2250523", + "fulfill (with selector): batch of 001": "93606", + "fulfill (with selector): batch of 002": "137034", + "fulfill (with selector): batch of 004": "225813", + "fulfill (with selector): batch of 008": "393628", + "fulfill (with selector): batch of 016": "730103", + "fulfill (with selector): batch of 032": "1428732", + "fulfill: batch of 001": "91345", + "fulfill: batch of 002": "132481", + "fulfill: batch of 004": "216760", + "fulfill: batch of 008": "375573", + "fulfill: batch of 016": "693456", + "fulfill: batch of 032": "1354660", + "fulfill: batch of 064": "2743065", + "fulfill: batch of 128": "5725531" +} \ No newline at end of file diff --git a/contracts/src/BoundlessMarket.sol b/contracts/src/BoundlessMarket.sol index 5c3b10221..2e875eb0d 100644 --- a/contracts/src/BoundlessMarket.sol +++ b/contracts/src/BoundlessMarket.sol @@ -12,6 +12,7 @@ import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/crypt import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; @@ -37,6 +38,7 @@ import {IBoundlessRouter} from "./router/interfaces/IBoundlessRouter.sol"; error InvalidRouter(); error InvalidCollateralToken(); +error InvalidLegacyImpl(); error InvalidInitialOwner(); error MismatchedRequestId(uint256 expected, uint256 received); @@ -45,7 +47,8 @@ contract BoundlessMarket is Initializable, EIP712Upgradeable, AccessControlUpgradeable, - UUPSUpgradeable + UUPSUpgradeable, + Proxy { using SafeCast for int256; using SafeCast for uint256; @@ -77,6 +80,16 @@ contract BoundlessMarket is /// @custom:oz-upgrades-unsafe-allow state-variable-immutable address public immutable COLLATERAL_TOKEN_CONTRACT; + /// @notice Implementation address of the previous (legacy ABI) BoundlessMarket. + /// The fallback function delegate-calls into this address so the + /// pre-router ABI keeps working for in-flight transactions and old + /// broker clients during the migration window. + /// @dev On Base mainnet this is the impl pointed to by the proxy before + /// the upgrade. On dev/localnet a fresh deployment of + /// contracts/src/legacy/BoundlessMarketLegacy.sol. + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address public immutable LEGACY_IMPL; + /// @notice Max gas allowed for ERC1271 smart contract signature checks used for client auth. /// @dev This constraint is applied to smart contract signatures used for authorizing proof /// requests in order to make gas costs bounded. @@ -96,16 +109,30 @@ contract BoundlessMarket is uint96 public constant MARKET_FEE_BPS = 0; /// @custom:oz-upgrades-unsafe-allow constructor - constructor(IBoundlessRouter router, address collateralTokenContract) { + constructor(IBoundlessRouter router, address collateralTokenContract, address legacyImpl) { if (address(router) == address(0)) revert InvalidRouter(); if (collateralTokenContract == address(0)) revert InvalidCollateralToken(); + if (legacyImpl == address(0)) revert InvalidLegacyImpl(); ROUTER = router; COLLATERAL_TOKEN_CONTRACT = collateralTokenContract; + LEGACY_IMPL = legacyImpl; _disableInitializers(); } + /// @notice OpenZeppelin {Proxy} hook: returns the address that selectors not + /// declared on this contract are delegate-called into. The inherited + /// `fallback()` forwards to it, preserving the caller, value, and the + /// proxy's storage context. + /// @dev This is the LEGACY ABI implementation, NOT this contract's ERC1967 + /// implementation (that lives in the proxy's storage slot). Keeping + /// the legacy ABI surface live this way avoids re-introducing the + /// legacy bodies into this implementation's bytecode. + function _implementation() internal view override returns (address) { + return LEGACY_IMPL; + } + function initialize(address initialOwner) external initializer { if (initialOwner == address(0)) { revert InvalidInitialOwner(); diff --git a/contracts/src/legacy/BoundlessMarketLegacy.sol b/contracts/src/legacy/BoundlessMarketLegacy.sol new file mode 100644 index 000000000..adf91c6dc --- /dev/null +++ b/contracts/src/legacy/BoundlessMarketLegacy.sol @@ -0,0 +1,962 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.26; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { + IRiscZeroVerifier, + Receipt, + ReceiptClaim, + ReceiptClaimLib, + VerificationFailed +} from "risc0/IRiscZeroVerifier.sol"; +import {IRiscZeroSetVerifier} from "risc0/IRiscZeroSetVerifier.sol"; + +import {IBoundlessMarket} from "./IBoundlessMarketLegacy.sol"; +import {IBoundlessMarketCallback} from "./IBoundlessMarketCallbackLegacy.sol"; +import {Account} from "./types/Account.sol"; +import {AssessorJournal} from "./types/AssessorJournal.sol"; +import {AssessorCallback} from "./types/AssessorCallback.sol"; +import {AssessorCommitment} from "./types/AssessorCommitment.sol"; +import {Fulfillment} from "./types/Fulfillment.sol"; +import {FulfillmentDataLibrary, FulfillmentDataType} from "./types/FulfillmentData.sol"; +import {AssessorReceipt} from "./types/AssessorReceipt.sol"; +import {ProofRequest} from "./types/ProofRequest.sol"; +import {LockRequestLibrary} from "./types/LockRequest.sol"; +import {RequestId} from "./types/RequestId.sol"; +import {RequestLock} from "./types/RequestLock.sol"; +import {FulfillmentContext, FulfillmentContextLibrary} from "./types/FulfillmentContext.sol"; + +import {BoundlessMarketLib} from "./libraries/BoundlessMarketLib.sol"; +import {MerkleProofish} from "./libraries/MerkleProofish.sol"; + +error InvalidVerifier(); +error InvalidApplicationVerifier(); +error InvalidAssessorImage(); +error InvalidDeprecatedAssessorImage(); +error InvalidCollateralToken(); +error InvalidInitialOwner(); + +contract BoundlessMarket is + IBoundlessMarket, + Initializable, + EIP712Upgradeable, + AccessControlUpgradeable, + UUPSUpgradeable +{ + using ReceiptClaimLib for ReceiptClaim; + using SafeCast for int256; + using SafeCast for uint256; + using SafeTransferLib for ERC20; + + /// @dev The version of the contract, with respect to upgrades. + uint64 public constant VERSION = 1; + + /// @notice Admin role identifier + bytes32 public constant ADMIN_ROLE = DEFAULT_ADMIN_ROLE; + + /// Mapping of request ID to lock-in state. Non-zero for requests that are locked in. + mapping(RequestId => RequestLock) public requestLocks; + /// Mapping of address to account state. + mapping(address => Account) internal accounts; + + // Using immutable here means the image ID and verifier address is linked to the implementation + // contract, and not to the proxy. Any deployment that wants to update these values must deploy + // a new implementation contract. + /// @dev Risc0 verifier router used for assessor seals. + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IRiscZeroVerifier public immutable VERIFIER; + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + bytes32 public immutable ASSESSOR_ID; + string private imageUrl; + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address public immutable COLLATERAL_TOKEN_CONTRACT; + + /// @notice Max gas allowed for verification of an application proof, when selector is default. + /// @dev If no selector is specified as part of the request's requirements, the prover must + /// provide a proof that can be verified with at most the amount of gas specified by this + /// constant. This requirement exists to ensure that by default, the client can then post the + /// given proof in a new transaction as part of the application. + uint256 public constant DEFAULT_MAX_GAS_FOR_VERIFY = 50000; + + /// @notice Max gas allowed for ERC1271 smart contract signature checks used for client auth. + /// @dev This constraint is applied to smart contract signatures used for authorizing proof + /// requests in order to make gas costs bounded. + uint256 public constant ERC1271_MAX_GAS_FOR_CHECK = 100000; + + /// @notice When a prover is slashed for failing to fulfill a request, a portion of the collateral + /// is burned, and the remaining portion is either send to the prover that ultimately fulfilled + /// the order, or to the market treasury. This fraction controls that ratio. + /// @dev The value is configured as a constant to avoid accessing storage and thus paying for the + /// gas of an SLOAD. Can only be changed via contract upgrade. + uint256 public constant SLASHING_BURN_BPS = 5000; + + /// @notice When an order is fulfilled, the market takes a fee based on the price of the order. + /// This fraction is multiplied by the price to decide the fee. + /// @dev The fee is configured as a constant to avoid accessing storage and thus paying for the + /// gas of an SLOAD. Can only be changed via contract upgrade. + uint96 public constant MARKET_FEE_BPS = 0; + + /// @notice The ID of the deprecated assessor image. + /// @dev After a contract upgrade, the ASSESSOR_ID might change, so this value is used to + /// keep active the previous version of the assessor until its expiration. In this way, + /// contract upgrades can be performed without disrupting ongoing fulfillments. + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + bytes32 public immutable DEPRECATED_ASSESSOR_ID; + + /// @notice The expiration timestamp of the deprecated assessor. + /// @dev This value is used to determine when the previous version of the assessor is no longer + /// active. Any assessor seals that were created with the deprecated image ID must be fulfilled + /// before this timestamp. + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + uint64 public immutable DEPRECATED_ASSESSOR_EXPIRES_AT; + + // Using immutable here means the application verifier address is linked to the implementation + // contract, and not to the proxy. Any deployment that wants to update this value must deploy + // a new implementation contract. + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IRiscZeroVerifier public immutable APPLICATION_VERIFIER; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor( + IRiscZeroVerifier verifier, + IRiscZeroVerifier applicationVerifier, + bytes32 assessorId, + bytes32 deprecatedAssessorId, + uint32 deprecatedAssessorDuration, + address collateralTokenContract + ) { + // Validate non-zero critical params + if (address(verifier) == address(0)) { + revert InvalidVerifier(); + } + if (address(applicationVerifier) == address(0)) { + revert InvalidApplicationVerifier(); + } + if (assessorId == bytes32(0)) { + revert InvalidAssessorImage(); + } + if (collateralTokenContract == address(0)) { + revert InvalidCollateralToken(); + } + if (deprecatedAssessorDuration > 0) { + if (deprecatedAssessorId == bytes32(0)) { + revert InvalidDeprecatedAssessorImage(); + } + } + + VERIFIER = verifier; + APPLICATION_VERIFIER = applicationVerifier; + ASSESSOR_ID = assessorId; + COLLATERAL_TOKEN_CONTRACT = collateralTokenContract; + DEPRECATED_ASSESSOR_ID = deprecatedAssessorId; + DEPRECATED_ASSESSOR_EXPIRES_AT = uint64(block.timestamp) + deprecatedAssessorDuration; + + _disableInitializers(); + } + + function initialize(address initialOwner, string calldata _imageUrl) external initializer { + if (initialOwner == address(0)) { + revert InvalidInitialOwner(); + } + __AccessControl_init(); + __UUPSUpgradeable_init(); + __EIP712_init(BoundlessMarketLib.EIP712_DOMAIN, BoundlessMarketLib.EIP712_DOMAIN_VERSION); + _grantRole(ADMIN_ROLE, initialOwner); + imageUrl = _imageUrl; + } + + function setImageUrl(string calldata _imageUrl) external onlyRole(ADMIN_ROLE) { + imageUrl = _imageUrl; + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(ADMIN_ROLE) {} + + // NOTE: We could verify the client signature here, but this adds about 18k gas (with a naive + // implementation), doubling the cost of calling this method. It is not required for protocol + // safety as the signature is checked during lock, and during fulfillment (by the assessor). + function submitRequest(ProofRequest calldata request, bytes calldata clientSignature) external payable { + if (msg.value > 0) { + deposit(); + } + emit RequestSubmitted(request.id, request, clientSignature); + } + + /// @inheritdoc IBoundlessMarket + function lockRequest(ProofRequest calldata request, bytes calldata clientSignature) external { + (address client, uint32 idx) = request.id.clientAndIndex(); + (bytes32 requestHash,) = _verifyClientSignature(request, client, clientSignature); + (uint64 lockDeadline, uint64 deadline) = request.validate(); + + _lockRequest(request, clientSignature, requestHash, client, idx, msg.sender, lockDeadline, deadline); + } + + /// @inheritdoc IBoundlessMarket + function lockRequestWithSignature( + ProofRequest calldata request, + bytes calldata clientSignature, + bytes calldata proverSignature + ) external { + (address client, uint32 idx) = request.id.clientAndIndex(); + (bytes32 requestHash, bytes32 proofRequestEip712Digest) = + _verifyClientSignature(request, client, clientSignature); + bytes32 lockRequestHash = + _hashTypedDataV4(LockRequestLibrary.eip712DigestFromPrecomputedDigest(proofRequestEip712Digest)); + address prover = ECDSA.recover(lockRequestHash, proverSignature); + (uint64 lockDeadline, uint64 deadline) = request.validate(); + + _lockRequest(request, clientSignature, requestHash, client, idx, prover, lockDeadline, deadline); + } + + /// @notice Locks the request to the prover. Deducts funds from the client for payment + /// and funding from the prover for locking collateral. + function _lockRequest( + ProofRequest calldata request, + bytes calldata clientSignature, + bytes32 requestDigest, + address client, + uint32 idx, + address prover, + uint64 lockDeadline, + uint64 deadline + ) internal { + (bool locked, bool fulfilled) = accounts[client].requestFlags(idx); + if (locked) { + revert RequestIsLocked({requestId: request.id}); + } + if (fulfilled) { + revert RequestIsFulfilled({requestId: request.id}); + } + if (block.timestamp > lockDeadline) { + revert RequestLockIsExpired({requestId: request.id, lockDeadline: lockDeadline}); + } + + // Compute the current price offered by the reverse Dutch auction. + uint96 price = request.offer.priceAt(uint64(block.timestamp)).toUint96(); + + // Deduct payment from the client account and collateral from the prover account. + Account storage clientAccount = accounts[client]; + if (clientAccount.balance < price) { + revert InsufficientBalance(client); + } + Account storage proverAccount = accounts[prover]; + if (proverAccount.collateralBalance < request.offer.lockCollateral) { + revert InsufficientBalance(prover); + } + + unchecked { + clientAccount.balance -= price; + proverAccount.collateralBalance -= request.offer.lockCollateral.toUint96(); + } + + // Record the lock for the request and emit an event. + requestLocks[request.id] = RequestLock({ + prover: prover, + price: price, + requestLockFlags: 0, + lockDeadline: lockDeadline, + deadlineDelta: uint256(deadline - lockDeadline).toUint24(), + collateral: request.offer.lockCollateral.toUint96(), + requestDigest: requestDigest + }); + + clientAccount.setRequestLocked(idx); + emit RequestLocked(request.id, prover, request, clientSignature); + } + + /// Validates the request and records the price to transient storage such that it can be + /// fulfilled within the same transaction without taking a lock on it. + /// @inheritdoc IBoundlessMarket + function priceRequest(ProofRequest calldata request, bytes calldata clientSignature) public { + address client = request.id.client(); + + (bytes32 requestHash,) = _verifyClientSignature(request, client, clientSignature); + + (, uint64 deadline) = request.validate(); + bool expired = deadline < block.timestamp; + + // Compute the current price offered by the reverse Dutch auction. + uint96 price = request.offer.priceAt(uint64(block.timestamp)).toUint96(); + + // Record the price in transient storage, such that the order can be filled in this same transaction. + FulfillmentContext({valid: true, expired: expired, price: price}).store(requestHash); + } + + /// @inheritdoc IBoundlessMarket + function verifyDelivery(Fulfillment[] calldata fills, AssessorReceipt calldata assessorReceipt) public view { + // TODO(#242): Figure out how much the memory here is costing. If it's significant, we can do some tricks to reduce memory pressure. + // We can't handle more than 65535 fills in a single batch. + // This is a limitation of the current Selector implementation, + // that uses a uint16 for the index, and can be increased in the future. + if (fills.length > type(uint16).max) { + revert BatchSizeExceedsLimit(fills.length, type(uint16).max); + } + bytes32[] memory leaves = new bytes32[](fills.length); + bool[] memory hasSelector = new bool[](fills.length); + + // Check the selector constraints. + // NOTE: The assessor guest adds non-zero selector values to the list. + uint256 selectorsLength = assessorReceipt.selectors.length; + for (uint256 i = 0; i < selectorsLength; i++) { + bytes4 expected = assessorReceipt.selectors[i].value; + bytes4 received = bytes4(fills[assessorReceipt.selectors[i].index].seal[0:4]); + hasSelector[assessorReceipt.selectors[i].index] = true; + if (expected != received) { + revert SelectorMismatch(expected, received); + } + } + + // Verify the application receipts. + for (uint256 i = 0; i < fills.length; i++) { + Fulfillment calldata fill = fills[i]; + bytes32 fulfillmentDataDigest = fill.fulfillmentDataDigest(); + + leaves[i] = AssessorCommitment(i, fill.id, fill.requestDigest, fill.claimDigest, fulfillmentDataDigest) + .eip712Digest(); + + // If the requestor did not specify a selector, we verify with DEFAULT_MAX_GAS_FOR_VERIFY gas limit. + // This ensures that by default, client receive proofs that can be verified cheaply as part of their applications. + if (!hasSelector[i]) { + APPLICATION_VERIFIER.verifyIntegrity{gas: DEFAULT_MAX_GAS_FOR_VERIFY}( + Receipt(fill.seal, fill.claimDigest) + ); + } else { + APPLICATION_VERIFIER.verifyIntegrity(Receipt(fill.seal, fill.claimDigest)); + } + } + + bytes32 batchRoot = MerkleProofish.processTree(leaves); + + // Verify the assessor, which ensures the application proof fulfills a valid request with the given ID. + // NOTE: Signature checks and recursive verification happen inside the assessor. + bytes32 assessorJournalDigest = sha256( + abi.encode( + AssessorJournal({ + root: batchRoot, + callbacks: assessorReceipt.callbacks, + selectors: assessorReceipt.selectors, + prover: assessorReceipt.prover + }) + ) + ); + // Verification of the assessor seal does not need to comply with DEFAULT_MAX_GAS_FOR_VERIFY. + try VERIFIER.verify(assessorReceipt.seal, ASSESSOR_ID, assessorJournalDigest) {} + catch { + if (block.timestamp > DEPRECATED_ASSESSOR_EXPIRES_AT) { + revert VerificationFailed(); + } + VERIFIER.verify(assessorReceipt.seal, DEPRECATED_ASSESSOR_ID, assessorJournalDigest); + } + } + + /// @inheritdoc IBoundlessMarket + function priceAndFulfill( + ProofRequest[] calldata requests, + bytes[] calldata clientSignatures, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) public returns (bytes[] memory paymentError) { + for (uint256 i = 0; i < requests.length; i++) { + priceRequest(requests[i], clientSignatures[i]); + } + paymentError = fulfill(fills, assessorReceipt); + } + + /// @inheritdoc IBoundlessMarket + function fulfill(Fulfillment[] calldata fills, AssessorReceipt calldata assessorReceipt) + public + returns (bytes[] memory paymentError) + { + verifyDelivery(fills, assessorReceipt); + + paymentError = new bytes[](fills.length); + + // Create reverse lookup index for fills to any associated callback. + uint256[] memory fillToCallbackIndexPlusOne = new uint256[](fills.length); + uint256 callbacksLength = assessorReceipt.callbacks.length; + for (uint256 i = 0; i < callbacksLength; i++) { + AssessorCallback calldata callback = assessorReceipt.callbacks[i]; + // Add one to the index such that zero indicates no callback. + fillToCallbackIndexPlusOne[callback.index] = i + 1; + } + + // NOTE: It could be slightly more efficient to keep balances and request flags in memory until a single + // batch update to storage. However, updating the same storage slot twice only costs 100 gas, so + // this savings is marginal, and will be outweighed by complicated memory management if not careful. + for (uint256 i = 0; i < fills.length; i++) { + Fulfillment calldata fill = fills[i]; + bool expired; + (paymentError[i], expired) = _fulfillAndPay(fill, assessorReceipt.prover); + + // Skip the callback if this fulfillment is related to an unlocked request. See the note + // in _fulfillAndPay for more details. This check could potentially be optimized, as it + // is duplicated in _fulfillAndPay. + if (expired) { + continue; + } + + uint256 callbackIndexPlusOne = fillToCallbackIndexPlusOne[i]; + if (callbackIndexPlusOne > 0) { + if (fill.fulfillmentDataType == FulfillmentDataType.ImageIdAndJournal) { + (bytes32 imageId, bytes calldata journal) = + FulfillmentDataLibrary.decodePackedImageIdAndJournal(fill.fulfillmentData); + AssessorCallback calldata callback = assessorReceipt.callbacks[callbackIndexPlusOne - 1]; + _executeCallback(fill.id, callback.addr, callback.gasLimit, imageId, journal, fill.seal); + } else { + // A callback was requested, but it cannot be fulfilled, so revert. + revert UnfulfillableCallback(); + } + } + } + } + + /// @inheritdoc IBoundlessMarket + function priceAndFulfillAndWithdraw( + ProofRequest[] calldata requests, + bytes[] calldata clientSignatures, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) public returns (bytes[] memory paymentError) { + for (uint256 i = 0; i < requests.length; i++) { + priceRequest(requests[i], clientSignatures[i]); + } + paymentError = fulfillAndWithdraw(fills, assessorReceipt); + } + + /// @inheritdoc IBoundlessMarket + function fulfillAndWithdraw(Fulfillment[] calldata fills, AssessorReceipt calldata assessorReceipt) + public + returns (bytes[] memory paymentError) + { + paymentError = fulfill(fills, assessorReceipt); + + // Withdraw any remaining balance from the prover account. + uint256 balance = accounts[assessorReceipt.prover].balance; + if (balance > 0) { + _withdraw(assessorReceipt.prover, balance); + } + } + + /// Complete the fulfillment logic after having verified the app and assessor receipts. + function _fulfillAndPay(Fulfillment calldata fill, address prover) + internal + returns (bytes memory paymentError, bool expired) + { + RequestId id = fill.id; + (address client, uint32 idx) = id.clientAndIndex(); + Account storage clientAccount = accounts[client]; + (bool locked, bool fulfilled) = clientAccount.requestFlags(idx); + + // Fetch the lock and fulfillment information. + // NOTE: The `lock` should only be used in code paths where locked is true. + RequestLock memory lock; + if (locked) { + lock = requestLocks[id]; + } + FulfillmentContext memory context = FulfillmentContextLibrary.load(fill.requestDigest); + + // First, check whether the request is known to be a valid signed request, and whether it is + // expired. If the request cannot be authenticated, revert. + // + // In the expired case, we return early here. We do not emit the ProofDelivered event, and + // we do not issue a callback. This makes interpretation of the ProofDelivered events + // simpler, as they cannot be emitted for an expired request. + if (context.valid) { + // Request has been validated in priceRequest, check the reported expiration. + if (context.expired) { + paymentError = abi.encodeWithSelector(RequestIsExpired.selector, RequestId.unwrap(id)); + emit PaymentRequirementsFailed(paymentError); + return (paymentError, true); + } + } else if (locked && lock.requestDigest == fill.requestDigest) { + // Request was validated in lockRequest, check whether the request is fully expired. + if (lock.deadline() < block.timestamp) { + paymentError = abi.encodeWithSelector(RequestIsExpired.selector, RequestId.unwrap(id)); + emit PaymentRequirementsFailed(paymentError); + return (paymentError, true); + } + } else { + // Request is not validated by either price or lock step. We cannot determine that the + // request is authentic, so we revert. + // NOTE: We could loosen this slightly, only reverting when the id indicates this is a + // smart-contract authorized request. However, we'd need to handle the fact that we + // don't have a FulfillmentContext on this code path. + revert RequestIsNotLockedOrPriced(id); + } + + // NOTE: Every code path past this point must ensure the `fulfilled` flag is set, or + // revert. If this is not the case, then it will break the invariant that the first + // delivered proof (e.g. the first time `ProofDelivered` fires and the first time the + // callback is called) the fulfilled flag is set. + if (locked) { + if (lock.lockDeadline >= block.timestamp) { + paymentError = _fulfillAndPayLocked(lock, id, client, idx, fill, fulfilled, prover); + } else { + // NOTE: If the request is not priced, the context will be all zeroes. We will have + // only reached this point if the request digest matches the lock, which is expired. + // In this case, the price will be zero, which is correct. + paymentError = _fulfillAndPayWasLocked(lock, id, client, idx, context.price, fill, fulfilled, prover); + } + } else { + paymentError = _fulfillAndPayNeverLocked(id, client, idx, context.price, fill, fulfilled, prover); + } + + if (paymentError.length > 0) { + emit PaymentRequirementsFailed(paymentError); + } + emit ProofDelivered(fill.id, prover, fill); + } + + /// @notice For a request that is currently locked. Marks the request as fulfilled, and transfers payment if eligible. + /// @dev It is possible for anyone to fulfill a request at any time while the request has not expired. + /// If the request is currently locked, only the prover can fulfill it and receive payment + function _fulfillAndPayLocked( + RequestLock memory lock, + RequestId id, + address client, + uint32 idx, + Fulfillment calldata fill, + bool fulfilled, + address assessorProver + ) internal returns (bytes memory paymentError) { + // NOTE: If the prover is paid, the fulfilled flag must be set. + if (lock.isProverPaid()) { + return abi.encodeWithSelector(RequestIsFulfilled.selector, RequestId.unwrap(id)); + } + + if (!fulfilled) { + accounts[client].setRequestFulfilled(idx); + emit RequestFulfilled(id, assessorProver, fill.requestDigest); + } + + // At this point the request has been fulfilled. The remaining logic determines whether + // payment should be sent and to whom. + // While the request is locked, only the locker is eligible for payment, and only for the request that was locked. + if (lock.prover != assessorProver || lock.requestDigest != fill.requestDigest) { + return abi.encodeWithSelector(RequestIsLocked.selector, RequestId.unwrap(id)); + } + requestLocks[id].setProverPaidBeforeLockDeadline(); + + uint96 price = lock.price; + if (MARKET_FEE_BPS > 0) { + price = _applyMarketFee(price); + } + accounts[assessorProver].balance += price; + accounts[assessorProver].collateralBalance += lock.collateral; + } + + /// @notice For a request that was locked, and now the lock has expired. Marks the request as fulfilled, + /// and transfers payment if eligible. + /// @dev It is possible for anyone to fulfill a request at any time while the request has not expired. + /// If the request was locked, and now the lock has expired, and the request as a whole has not expired, + /// anyone can fulfill it and receive payment. + function _fulfillAndPayWasLocked( + RequestLock memory lock, + RequestId id, + address client, + uint32 idx, + uint96 price, + Fulfillment calldata fill, + bool fulfilled, + address assessorProver + ) internal returns (bytes memory paymentError) { + // NOTE: If the prover is paid, the fulfilled flag must be set. + if (lock.isProverPaid()) { + return abi.encodeWithSelector(RequestIsFulfilled.selector, RequestId.unwrap(id)); + } + + if (!fulfilled) { + accounts[client].setRequestFulfilled(idx); + emit RequestFulfilled(id, assessorProver, fill.requestDigest); + } + + // Deduct any additionally owned funds from client account. The client was already charged + // for the price at lock time once when the request was locked. We only need to charge any + // additional price for the difference between the price of the fulfilled request, at the + // current block, and the price of the locked request. + // + // Note that although they have the same ID, the locked request and the fulfilled request + // could be different. If the request fulfilled is the same as the one locked, the + // price will be zero and the entire fee on the lock will be returned to the client. + Account storage clientAccount = accounts[client]; + + // If the request has the same id, but is different to the request that was locked, the fulfillment + // price could be either higher or lower than the price that was previously locked. + // If the price is higher, we charge the client the difference. + // If the price is lower, we refund the client the difference. + uint96 lockPrice = lock.price; + bool partialPayment = false; + uint96 finalPrice = price; + + if (price > lockPrice) { + uint96 clientOwes = price - lockPrice; + if (clientAccount.balance < clientOwes) { + // If the client does not have enough balance to cover the full amount owed, + // we will only charge them what they have available. + clientOwes = clientAccount.balance; + finalPrice = lockPrice + clientOwes; + partialPayment = true; + } + unchecked { + clientAccount.balance -= clientOwes; + } + } else { + uint96 clientOwed = lockPrice - price; + clientAccount.balance += clientOwed; + } + + requestLocks[id].setProverPaidAfterLockDeadline(assessorProver); + if (MARKET_FEE_BPS > 0) { + finalPrice = _applyMarketFee(finalPrice); + } + accounts[assessorProver].balance += finalPrice; + if (partialPayment) { + return abi.encodeWithSelector(PartialPayment.selector, price, finalPrice); + } + } + + /// @notice For a request that has never been locked. Marks the request as fulfilled, and transfers payment if eligible. + /// @dev If a never locked request is fulfilled, but client has not enough funds to cover the payment, no + /// payment can ever be rendered for this order in the future. + function _fulfillAndPayNeverLocked( + RequestId id, + address client, + uint32 idx, + uint96 price, + Fulfillment calldata fill, + bool fulfilled, + address assessorProver + ) internal returns (bytes memory paymentError) { + // When never locked, the fulfilled flag _does_ indicate that we alrady attempted to + // transfer payment (which will only fail in the InsufficientBalance case below) so we + // return early here. + if (fulfilled) { + return abi.encodeWithSelector(RequestIsFulfilled.selector, RequestId.unwrap(id)); + } + + Account storage clientAccount = accounts[client]; + clientAccount.setRequestFulfilled(idx); + emit RequestFulfilled(id, assessorProver, fill.requestDigest); + + // Deduct the funds from client account. + // NOTE: In the case of InsufficientBalance, the payment can never be transferred in the + // future. This is a simplifying choice. + if (clientAccount.balance < price) { + return abi.encodeWithSelector(InsufficientBalance.selector, client); + } + unchecked { + clientAccount.balance -= price; + } + + if (MARKET_FEE_BPS > 0) { + price = _applyMarketFee(price); + } + accounts[assessorProver].balance += price; + } + + function _applyMarketFee(uint96 proverPayment) internal returns (uint96) { + uint96 fee = proverPayment * MARKET_FEE_BPS / 10000; + accounts[address(this)].balance += fee; + return proverPayment - fee; + } + + /// @notice Execute the callback for a fulfilled request if one is specified + /// @dev This function is called after payment is processed and handles any callback specified in the request + /// @param id The ID of the request being fulfilled + /// @param callbackAddr The address of the callback contract + /// @param callbackGasLimit The gas limit to use for the callback + /// @param imageId The ID of the RISC Zero guest image that produced the proof + /// @param journal The output journal from the RISC Zero guest execution + /// @param seal The cryptographic seal proving correct execution + function _executeCallback( + RequestId id, + address callbackAddr, + uint96 callbackGasLimit, + bytes32 imageId, + bytes calldata journal, + bytes calldata seal + ) internal { + // Ensure sufficient gas for callback, accounting for EIP-150 (63/64 rule). + // The requestor is responsible for ensuring that the callback gas limit is sufficient to cover + // for any extra overhead that the caller pays (calldata copy, cold access, etc.). + if (gasleft() * 63 / 64 < callbackGasLimit) revert InsufficientGas(); + try IBoundlessMarketCallback(callbackAddr).handleProof{gas: callbackGasLimit}(imageId, journal, seal) {} + catch (bytes memory err) { + emit CallbackFailed(id, callbackAddr, err); + } + } + + /// @inheritdoc IBoundlessMarket + function submitRoot(address setVerifierAddress, bytes32 root, bytes calldata seal) external { + IRiscZeroSetVerifier(address(setVerifierAddress)).submitMerkleRoot(root, seal); + } + + /// @inheritdoc IBoundlessMarket + function submitRootAndFulfill( + address setVerifier, + bytes32 root, + bytes calldata seal, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) external returns (bytes[] memory paymentError) { + IRiscZeroSetVerifier(address(setVerifier)).submitMerkleRoot(root, seal); + paymentError = fulfill(fills, assessorReceipt); + } + + /// @inheritdoc IBoundlessMarket + function submitRootAndFulfillAndWithdraw( + address setVerifier, + bytes32 root, + bytes calldata seal, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) external returns (bytes[] memory paymentError) { + IRiscZeroSetVerifier(address(setVerifier)).submitMerkleRoot(root, seal); + paymentError = fulfillAndWithdraw(fills, assessorReceipt); + } + + /// @inheritdoc IBoundlessMarket + function submitRootAndPriceAndFulfill( + address setVerifier, + bytes32 root, + bytes calldata seal, + ProofRequest[] calldata requests, + bytes[] calldata clientSignatures, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) external returns (bytes[] memory paymentError) { + IRiscZeroSetVerifier(address(setVerifier)).submitMerkleRoot(root, seal); + paymentError = priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + } + + /// @inheritdoc IBoundlessMarket + function submitRootAndPriceAndFulfillAndWithdraw( + address setVerifier, + bytes32 root, + bytes calldata seal, + ProofRequest[] calldata requests, + bytes[] calldata clientSignatures, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) external returns (bytes[] memory paymentError) { + IRiscZeroSetVerifier(address(setVerifier)).submitMerkleRoot(root, seal); + paymentError = priceAndFulfillAndWithdraw(requests, clientSignatures, fills, assessorReceipt); + } + + /// @inheritdoc IBoundlessMarket + function slash(RequestId requestId) external { + (address client, uint32 idx) = requestId.clientAndIndex(); + (bool locked,) = accounts[client].requestFlags(idx); + if (!locked) { + revert RequestIsNotLocked({requestId: requestId}); + } + + RequestLock memory lock = requestLocks[requestId]; + if (lock.isSlashed()) { + revert RequestIsSlashed({requestId: requestId}); + } + if (lock.isProverPaidBeforeLockDeadline()) { + revert RequestIsFulfilled({requestId: requestId}); + } + + // You can only slash a request after the request fully expires, so that if the request + // does get fulfilled, we know which prover should receive a portion of the collateral. + if (block.timestamp <= lock.deadline()) { + revert RequestIsNotExpired({requestId: requestId, deadline: lock.deadline()}); + } + + // Request was either fulfilled after the lock deadline or the request expired unfulfilled. + // In both cases the locker should be slashed. + requestLocks[requestId].setSlashed(); + + // Calculate the portion of collateral that should be burned vs sent to the prover. + uint256 burnValue = uint256(lock.collateral) * SLASHING_BURN_BPS / 10000; + + // If a prover fulfilled the request after the lock deadline, that prover + // receives the unburned portion of the collateral as a reward. + // Otherwise the request expired unfulfilled, unburnt collateral accrues to the market treasury, + // and we refund the client the price they paid for the request at lock time. + uint96 transferValue = (uint256(lock.collateral) - burnValue).toUint96(); + address collateralRecipient = lock.prover; + if (lock.isProverPaidAfterLockDeadline()) { + // At this point lock.prover is the prover that ultimately fulfilled the request, not + // the prover that locked the request. Transfer them the unburnt collateral. + accounts[collateralRecipient].collateralBalance += transferValue; + } else { + collateralRecipient = address(this); + accounts[collateralRecipient].collateralBalance += transferValue; + accounts[client].balance += lock.price; + } + + ERC20(COLLATERAL_TOKEN_CONTRACT).transfer(address(0xdEaD), burnValue); + (burnValue); + emit ProverSlashed(requestId, burnValue, transferValue, collateralRecipient); + } + + /// @inheritdoc IBoundlessMarket + function imageInfo() external view returns (bytes32, string memory) { + return (ASSESSOR_ID, imageUrl); + } + + /// @inheritdoc IBoundlessMarket + function deposit() public payable { + accounts[msg.sender].balance += msg.value.toUint96(); + emit Deposit(msg.sender, msg.value); + } + + /// @inheritdoc IBoundlessMarket + function depositTo(address to) public payable { + accounts[to].balance += msg.value.toUint96(); + emit Deposit(to, msg.value); + } + + function _withdraw(address account, uint256 value) internal { + if (accounts[account].balance < value.toUint96()) { + revert InsufficientBalance(account); + } + unchecked { + accounts[account].balance -= value.toUint96(); + } + (bool sent,) = account.call{value: value}(""); + if (!sent) { + revert TransferFailed(); + } + emit Withdrawal(account, value); + } + + /// @inheritdoc IBoundlessMarket + function withdraw(uint256 value) public { + _withdraw(msg.sender, value); + } + + /// @inheritdoc IBoundlessMarket + function balanceOf(address addr) public view returns (uint256) { + return uint256(accounts[addr].balance); + } + + /// @inheritdoc IBoundlessMarket + function depositCollateral(uint256 value) external { + // Transfer tokens from user to market + _depositCollateral(msg.sender, msg.sender, value); + } + + /// @inheritdoc IBoundlessMarket + function depositCollateralTo(address to, uint256 value) external { + _depositCollateral(msg.sender, to, value); + } + + /// @inheritdoc IBoundlessMarket + function depositCollateralWithPermit(uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external { + // Transfer tokens from user to market + try ERC20(COLLATERAL_TOKEN_CONTRACT).permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {} + _depositCollateral(msg.sender, msg.sender, value); + } + + /// @inheritdoc IBoundlessMarket + function depositCollateralWithPermitTo(address to, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + { + try ERC20(COLLATERAL_TOKEN_CONTRACT).permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {} + _depositCollateral(msg.sender, to, value); + } + + function _depositCollateral(address from, address to, uint256 value) internal { + ERC20(COLLATERAL_TOKEN_CONTRACT).safeTransferFrom(from, address(this), value); + accounts[to].collateralBalance += value.toUint96(); + emit CollateralDeposit(to, value); + } + + /// @inheritdoc IBoundlessMarket + function withdrawCollateral(uint256 value) public { + if (accounts[msg.sender].collateralBalance < value.toUint96()) { + revert InsufficientBalance(msg.sender); + } + unchecked { + accounts[msg.sender].collateralBalance -= value.toUint96(); + } + // Transfer tokens from market to user + bool success = ERC20(COLLATERAL_TOKEN_CONTRACT).transfer(msg.sender, value); + if (!success) revert TransferFailed(); + + emit CollateralWithdrawal(msg.sender, value); + } + + /// @inheritdoc IBoundlessMarket + function balanceOfCollateral(address addr) public view returns (uint256) { + return uint256(accounts[addr].collateralBalance); + } + + /// @inheritdoc IBoundlessMarket + function requestIsFulfilled(RequestId id) public view returns (bool) { + (address client, uint32 idx) = id.clientAndIndex(); + (, bool fulfilled) = accounts[client].requestFlags(idx); + return fulfilled; + } + + /// @inheritdoc IBoundlessMarket + function requestIsLocked(RequestId id) public view returns (bool) { + (address client, uint32 idx) = id.clientAndIndex(); + (bool locked,) = accounts[client].requestFlags(idx); + return locked; + } + + /// @inheritdoc IBoundlessMarket + function requestIsSlashed(RequestId id) external view returns (bool) { + return requestLocks[id].isSlashed(); + } + + /// @inheritdoc IBoundlessMarket + function requestLockDeadline(RequestId id) external view returns (uint64) { + if (!requestIsLocked(id)) { + revert RequestIsNotLocked({requestId: id}); + } + return requestLocks[id].lockDeadline; + } + + /// @inheritdoc IBoundlessMarket + function requestDeadline(RequestId id) external view returns (uint64) { + if (!requestIsLocked(id)) { + revert RequestIsNotLocked({requestId: id}); + } + return requestLocks[id].deadline(); + } + + function _verifyClientSignature(ProofRequest calldata request, address addr, bytes calldata clientSignature) + internal + view + returns (bytes32, bytes32) + { + bytes32 eip712Digest = request.eip712Digest(); + bytes32 requestHash = _hashTypedDataV4(eip712Digest); + if (request.id.isSmartContractSigned()) { + if ( + IERC1271(addr).isValidSignature{gas: ERC1271_MAX_GAS_FOR_CHECK}(requestHash, clientSignature) + != IERC1271.isValidSignature.selector + ) { + revert IBoundlessMarket.InvalidSignature(); + } + } else { + if (ECDSA.recover(requestHash, clientSignature) != addr) { + revert IBoundlessMarket.InvalidSignature(); + } + } + return (requestHash, eip712Digest); + } + + /// @inheritdoc IBoundlessMarket + function eip712DomainSeparator() external view returns (bytes32) { + return _domainSeparatorV4(); + } +} diff --git a/contracts/src/legacy/IBoundlessMarketCallbackLegacy.sol b/contracts/src/legacy/IBoundlessMarketCallbackLegacy.sol new file mode 100644 index 000000000..6e0194a31 --- /dev/null +++ b/contracts/src/legacy/IBoundlessMarketCallbackLegacy.sol @@ -0,0 +1,16 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +/// @title IBoundlessMarketCallback +/// @notice Interface for handling proof callbacks from BoundlessMarket with proof verification +/// @dev Inherit from this contract to implement custom proof handling logic for BoundlessMarket proofs +interface IBoundlessMarketCallback { + /// @notice Handles submitting proofs with RISC Zero proof verification + /// @param imageId The ID of the RISC Zero guest image that produced the proof + /// @param journal The output journal from the RISC Zero guest execution + /// @param seal The cryptographic seal proving correct execution + function handleProof(bytes32 imageId, bytes calldata journal, bytes calldata seal) external; +} diff --git a/contracts/src/legacy/IBoundlessMarketLegacy.sol b/contracts/src/legacy/IBoundlessMarketLegacy.sol new file mode 100644 index 000000000..c997009d2 --- /dev/null +++ b/contracts/src/legacy/IBoundlessMarketLegacy.sol @@ -0,0 +1,447 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pragma solidity ^0.8.26; + +import {Fulfillment} from "./types/Fulfillment.sol"; +import {AssessorReceipt} from "./types/AssessorReceipt.sol"; +import {ProofRequest} from "./types/ProofRequest.sol"; +import {RequestId} from "./types/RequestId.sol"; + +interface IBoundlessMarket { + /// @notice Event logged when a new proof request is submitted by a client. + /// @dev Note that the signature is not verified by the contract and should instead be verified + /// by the receiver of the event. + /// @param requestId The ID of the request. + /// @param request The proof request details. + /// @param clientSignature The signature of the client. + event RequestSubmitted(RequestId indexed requestId, ProofRequest request, bytes clientSignature); + + /// @notice Event logged when a request is locked in by the given prover. + /// @param requestId The ID of the request. + /// @param prover The address of the prover. + /// @param request The full proof request details. + /// @param clientSignature The signature of the client. + event RequestLocked(RequestId indexed requestId, address prover, ProofRequest request, bytes clientSignature); + + /// @notice Event logged when a request is fulfilled. + /// @param requestId The ID of the request. + /// @param prover The address of the prover fulfilling the request. + /// @param requestDigest The digest of the request. + event RequestFulfilled(RequestId indexed requestId, address indexed prover, bytes32 requestDigest); + + /// @notice Event logged when a proof is delivered that satisfies the request's requirements. + /// @dev It is possible for this event to be logged multiple times for a single request. The + /// first event logged will always coincide with the `RequestFulfilled` event and the fulfilled flag on the request being set. + /// @param requestId The ID of the request. + /// @param prover The address of the prover delivering the proof. + /// @param fulfillment The fulfillment details. + event ProofDelivered(RequestId indexed requestId, address indexed prover, Fulfillment fulfillment); + + /// Event when a prover is slashed is made to the market. + /// @param requestId The ID of the request. + /// @param collateralBurned The amount of collateral burned. + /// @param collateralTransferred The amount of collateral transferred to either the fulfilling prover or the market. + /// @param collateralRecipient The address of the collateral recipient. Typically the fulfilling prover, but can be the market. + event ProverSlashed( + RequestId indexed requestId, + uint256 collateralBurned, + uint256 collateralTransferred, + address collateralRecipient + ); + + /// @notice Event when a deposit is made to the market. + /// @param account The account making the deposit. + /// @param value The value of the deposit. + event Deposit(address indexed account, uint256 value); + + /// @notice Event when a withdrawal is made from the market. + /// @param account The account making the withdrawal. + /// @param value The value of the withdrawal. + event Withdrawal(address indexed account, uint256 value); + /// @notice Event when a collateral deposit is made to the market. + /// @param account The account making the deposit. + /// @param value The value of the deposit. + event CollateralDeposit(address indexed account, uint256 value); + /// @notice Event when a collateral withdrawal is made to the market. + /// @param account The account making the withdrawal. + /// @param value The value of the withdrawal. + event CollateralWithdrawal(address indexed account, uint256 value); + + /// @notice Event when the contract is upgraded to a new version. + /// @param version The new version of the contract. + event Upgraded(uint64 indexed version); + + /// @notice Event emitted during fulfillment if a request was fulfilled, but payment was not + /// transferred because at least one condition was not met. See the documentation on + /// `IBoundlessMarket.fulfill` for more information. + /// @dev The payload of the event is an ABI encoded error, from the errors on this contract. + /// If there is an unexpired lock on the request, the order, the prover holding the lock may + /// still be able to receive payment by sending another transaction. + /// @param error The ABI encoded error. + event PaymentRequirementsFailed(bytes error); + + /// @notice Event emitted when a callback to a contract fails during fulfillment + /// @param requestId The ID of the request that was being fulfilled + /// @param callback The address of the callback contract that failed + /// @param error The error message from the failed call + event CallbackFailed(RequestId indexed requestId, address callback, bytes error); + + /// @notice Error when a request is locked when it was not required to be. + /// @param requestId The ID of the request. + /// @dev selector 0xa9057651 + error RequestIsLocked(RequestId requestId); + + /// @notice Error when a request is not locked or priced during a fulfillment. + /// Either locking the request, or calling the `IBoundlessMarket.priceRequest` function + /// in the same transaction will satisfy this requirement. + /// @param requestId The ID of the request. + /// @dev selector 0xc274d3e3 + error RequestIsNotLockedOrPriced(RequestId requestId); + + /// @notice Error when a request is not locked when it was required to be. + /// @param requestId The ID of the request. + /// @dev selector d2be005d + error RequestIsNotLocked(RequestId requestId); + + /// @notice Error when a request is fulfilled when it was not required to be. + /// @param requestId The ID of the request. + /// @dev selector 0x1cfdeebb + error RequestIsFulfilled(RequestId requestId); + + /// @notice Error when a request is slashed when it was not required to be. + /// @param requestId The ID of the request. + /// @dev selector 0x64620c9a + error RequestIsSlashed(RequestId requestId); + + /// @notice Error when a request lock is no longer valid, as the lock deadline has passed. + /// @param requestId The ID of the request. + /// @param lockDeadline The lock deadline of the request. + /// @dev selector 0xcfe6a8fd + error RequestLockIsExpired(RequestId requestId, uint64 lockDeadline); + + /// @notice Error when a request is no longer valid, as the deadline has passed. + /// @param requestId The ID of the request. + /// @param deadline The deadline of the request. + /// @dev selector 0x873fd26b + error RequestIsExpired(RequestId requestId, uint64 deadline); + + /// @notice Error when a request is still valid, as the deadline has yet to pass. + /// @param requestId The ID of the request. + /// @param deadline The deadline of the request. + /// @dev selector 0x79c66ab0 + error RequestIsNotExpired(RequestId requestId, uint64 deadline); + + /// @notice Error when unable to complete request because of insufficient balance. + /// @param account The account with insufficient balance. + /// @dev selector 0x897f6c58 + error InsufficientBalance(address account); + + /// @notice Error when a payment is partially settled due to insufficient funds. + /// @param fullAmount The full amount that was required. + /// @param paidAmount The amount that was actually paid. + /// @dev selector 0x6008fdcb + error PartialPayment(uint256 fullAmount, uint256 paidAmount); + + /// @notice Error when a signature did not pass verification checks. + /// @dev selector 0x8baa579f + error InvalidSignature(); + + /// @notice Error when a request is malformed or internally inconsistent. + /// @dev selector 0x41abc801 + error InvalidRequest(); + + /// @notice Error when transfer of funds to an external address fails. + /// @dev selector 0x90b8ec18 + error TransferFailed(); + + /// @notice Error when providing a seal with a different selector than required. + /// @dev selector 0xb8b38d4c + error SelectorMismatch(bytes4 required, bytes4 provided); + + /// @notice Error when the batch size exceeds the limit. + /// @dev selector efc954a6 + error BatchSizeExceedsLimit(uint256 batchSize, uint256 limit); + + /// @notice Error when the fulfillment has a unfulfillable callback + /// @dev selector 0xb90a25b1 + error UnfulfillableCallback(); + + /// @notice Error when there is not enough gas to fulfill a callback. + /// @dev selector 0x1c26714c + error InsufficientGas(); + + /// @notice Check if the given request has been locked (i.e. accepted) by a prover. + /// @dev When a request is locked, only the prover it is locked to can be paid to fulfill the job. + /// @param requestId The ID of the request. + /// @return True if the request is locked, false otherwise. + function requestIsLocked(RequestId requestId) external view returns (bool); + + /// @notice Check if the given request resulted in the prover being slashed + /// (i.e. request was locked in but proof was not delivered) + /// @dev Note it is possible for a request to result in a slash, but still be fulfilled + /// if for example another prover decided to fulfill the request altruistically. + /// This function should not be used to determine if a request was fulfilled. + /// @param requestId The ID of the request. + /// @return True if the request resulted in the prover being slashed, false otherwise. + function requestIsSlashed(RequestId requestId) external view returns (bool); + + /// @notice Check if the given request has been fulfilled (i.e. a proof was delivered). + /// @param requestId The ID of the request. + /// @return True if the request is fulfilled, false otherwise. + function requestIsFulfilled(RequestId requestId) external view returns (bool); + + /// @notice For a given locked request, returns when the lock expires. + /// @dev If the request is not locked, this function will revert. + /// @param requestId The ID of the request. + /// @return The expiration time of the lock on the request. + function requestLockDeadline(RequestId requestId) external view returns (uint64); + + /// @notice For a given locked request, returns when request expires. + /// @dev If the request is not locked, this function will revert. + /// @param requestId The ID of the request. + /// @return The expiration time of the request. + function requestDeadline(RequestId requestId) external view returns (uint64); + + /// @notice Deposit Ether into the market to pay for proof. + /// @dev Value deposited is msg.value and it is credited to the account of msg.sender. + function deposit() external payable; + + /// @notice Deposit Ether into the market to pay for proof. + /// @dev Value deposited is msg.value and it is credited to the given account. + /// @param to The address to credit the deposit to. + function depositTo(address to) external payable; + + /// @notice Withdraw Ether from the market. + /// @dev Value is debited from msg.sender. + /// @param value The amount to withdraw. + function withdraw(uint256 value) external; + + /// @notice Check the deposited balance, in Ether, of the given account. + /// @param addr The address of the account. + /// @return The balance of the account. + function balanceOf(address addr) external view returns (uint256); + + /// @notice Deposit collateral into the market to pay for lockin collateral. + /// @dev Before calling this method, the account owner must approve the contract as an allowed spender. + function depositCollateral(uint256 value) external; + + /// @notice Deposit collateral into the market for another account to pay for lockin collateral. + /// @dev Before calling this method, the account owner must approve the contract as an allowed spender. + function depositCollateralTo(address to, uint256 value) external; + + /// @notice Permit and deposit collateral into the market to pay for lockin collateral. + /// @dev This method requires a valid EIP-712 signature from the account owner. + function depositCollateralWithPermit(uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; + + /// @notice Permit and deposit collateral into the market for another account to pay for lockin collateral. + /// @dev This method requires a valid EIP-712 signature from the account owner. + function depositCollateralWithPermitTo(address to, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + + /// @notice Withdraw collateral from the market. + function withdrawCollateral(uint256 value) external; + /// @notice Check the deposited balance, in HP, of the given account. + function balanceOfCollateral(address addr) external view returns (uint256); + + /// @notice Submit a request such that it is publicly available for provers to evaluate and bid on. + /// Any `msg.value` sent with the call will be added to the balance of `msg.sender`. + /// @dev Submitting the transaction only broadcasts it, and is not a required step. + /// This method does not validate the signature or store any state related to the request. + /// Verifying the signature here is not required for protocol safety as the signature is + /// checked when the request is locked, and during fulfillment (by the assessor). + /// @param request The proof request details. + /// @param clientSignature The signature of the client. + function submitRequest(ProofRequest calldata request, bytes calldata clientSignature) external payable; + + /// @notice Lock the request to the prover, giving them exclusive rights to be paid to + /// fulfill this request, and also making them subject to slashing penalties if they fail to + /// deliver. At this point, the price for fulfillment is also set, based on the reverse Dutch + /// auction parameters and the time at which this transaction is processed. + /// @dev This method should be called from the address of the prover. + /// @param request The proof request details. + /// @param clientSignature The signature of the client. + function lockRequest(ProofRequest calldata request, bytes calldata clientSignature) external; + + /// @notice Lock the request to the prover, giving them exclusive rights to be paid to + /// fulfill this request, and also making them subject to slashing penalties if they fail to + /// deliver. At this point, the price for fulfillment is also set, based on the reverse Dutch + /// auction parameters and the time at which this transaction is processed. + /// @dev This method uses the provided signature to authenticate the prover. + /// @param request The proof request details. + /// @param clientSignature The signature of the client. + /// @param proverSignature The signature of the prover. + function lockRequestWithSignature( + ProofRequest calldata request, + bytes calldata clientSignature, + bytes calldata proverSignature + ) external; + + /// @notice Fulfills a batch of requests. See IBoundlessMarket.fulfill for more information. + /// @param fills The array of fulfillment information. + /// @param assessorReceipt The Assessor's guest fulfillment information verified to confirm the + /// request's requirements are met. + function fulfill(Fulfillment[] calldata fills, AssessorReceipt calldata assessorReceipt) + external + returns (bytes[] memory paymentError); + + /// @notice Fulfills a batch of requests and withdraw from the prover balance. See IBoundlessMarket.fulfill for more information. + /// @param fills The array of fulfillment information. + /// @param assessorReceipt The Assessor's guest fulfillment information verified to confirm the + /// request's requirements are met. + function fulfillAndWithdraw(Fulfillment[] calldata fills, AssessorReceipt calldata assessorReceipt) + external + returns (bytes[] memory paymentError); + + /// @notice Verify the application and assessor receipts for the batch, ensuring that the provided + /// fulfillments satisfy the requests. + /// @param fills The array of fulfillment information. + /// @param assessorReceipt The Assessor's guest fulfillment information verified to confirm the + /// request's requirements are met. + function verifyDelivery(Fulfillment[] calldata fills, AssessorReceipt calldata assessorReceipt) external view; + + /// @notice Checks the validity of the request and then writes the current auction price to + /// transient storage. + /// @dev When called within the same transaction, this method can be used to fulfill a request + /// that is not locked. This is useful when the prover wishes to fulfill a request, but does + /// not want to issue a lock transaction e.g. because the collateral is too high or to save money by + /// avoiding the gas costs of the lock transaction. + /// @param request The proof request details. + /// @param clientSignature The signature of the client. + function priceRequest(ProofRequest calldata request, bytes calldata clientSignature) external; + + /// @notice A combined call to `IBoundlessMarket.priceRequest` and `IBoundlessMarket.fulfill`. + /// The caller should provide the signed request and signature for each unlocked request they + /// want to fulfill. Payment for unlocked requests will go to the provided `prover` address. + /// @param requests The array of proof requests. + /// @param clientSignatures The array of client signatures. + /// @param fills The array of fulfillment information. + /// @param assessorReceipt The Assessor's guest fulfillment information verified to confirm the + /// request's requirements are met. + function priceAndFulfill( + ProofRequest[] calldata requests, + bytes[] calldata clientSignatures, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) external returns (bytes[] memory paymentError); + + /// @notice A combined call to `IBoundlessMarket.priceRequest` and `IBoundlessMarket.fulfillAndWithdraw`. + /// The caller should provide the signed request and signature for each unlocked request they + /// want to fulfill. Payment for unlocked requests will go to the provided `prover` address. + /// @param requests The array of proof requests. + /// @param clientSignatures The array of client signatures. + /// @param fills The array of fulfillment information. + /// @param assessorReceipt The Assessor's guest fulfillment information verified to confirm the + /// request's requirements are met. + function priceAndFulfillAndWithdraw( + ProofRequest[] calldata requests, + bytes[] calldata clientSignatures, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) external returns (bytes[] memory paymentError); + + /// @notice Submit a new root to a set-verifier. + /// @dev Consider using `submitRootAndFulfill` to submit the root and fulfill in one transaction. + /// @param setVerifier The address of the set-verifier contract. + /// @param root The new merkle root. + /// @param seal The seal of the new merkle root. + function submitRoot(address setVerifier, bytes32 root, bytes calldata seal) external; + + /// @notice Combined function to submit a new root to a set-verifier and call fulfill. + /// @dev Useful to reduce the transaction count for fulfillments. + /// @param setVerifier The address of the set-verifier contract. + /// @param root The new merkle root. + /// @param seal The seal of the new merkle root. + /// @param fills The array of fulfillment information. + /// @param assessorReceipt The Assessor's guest fulfillment information verified to confirm the + /// request's requirements are met. + function submitRootAndFulfill( + address setVerifier, + bytes32 root, + bytes calldata seal, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) external returns (bytes[] memory paymentError); + + /// @notice Combined function to submit a new root to a set-verifier and call fulfillAndWithdraw. + /// @dev Useful to reduce the transaction count for fulfillments. + /// @param setVerifier The address of the set-verifier contract. + /// @param root The new merkle root. + /// @param seal The seal of the new merkle root. + /// @param fills The array of fulfillment information. + /// @param assessorReceipt The Assessor's guest fulfillment information verified to confirm the + /// request's requirements are met. + function submitRootAndFulfillAndWithdraw( + address setVerifier, + bytes32 root, + bytes calldata seal, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) external returns (bytes[] memory paymentError); + + /// @notice Combined function to submit a new root to a set-verifier and call priceAndFulfill. + /// @dev Useful to reduce the transaction count for fulfillments. + /// @param setVerifier The address of the set-verifier contract. + /// @param root The new merkle root. + /// @param seal The seal of the new merkle root. + /// @param fills The array of fulfillment information. + /// @param assessorReceipt The Assessor's guest fulfillment information verified to confirm the + /// request's requirements are met. + function submitRootAndPriceAndFulfill( + address setVerifier, + bytes32 root, + bytes calldata seal, + ProofRequest[] calldata requests, + bytes[] calldata clientSignatures, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) external returns (bytes[] memory paymentError); + + /// @notice Combined function to submit a new root to a set-verifier and call priceAndFulfillAndWithdraw. + /// @dev Useful to reduce the transaction count for fulfillments. + /// @param setVerifier The address of the set-verifier contract. + /// @param root The new merkle root. + /// @param seal The seal of the new merkle root. + /// @param fills The array of fulfillment information. + /// @param assessorReceipt The Assessor's guest fulfillment information verified to confirm the + /// request's requirements are met. + function submitRootAndPriceAndFulfillAndWithdraw( + address setVerifier, + bytes32 root, + bytes calldata seal, + ProofRequest[] calldata requests, + bytes[] calldata clientSignatures, + Fulfillment[] calldata fills, + AssessorReceipt calldata assessorReceipt + ) external returns (bytes[] memory paymentError); + + /// @notice When a prover fails to fulfill a request by the deadline, this method can be used to burn + /// the associated prover collateral. + /// @dev The provers collateral has already been transferred to the contract when the request was locked. + /// This method just burn the collateral. + /// @param requestId The ID of the request. + function slash(RequestId requestId) external; + + /// @notice EIP 712 domain separator getter. + /// @return The EIP 712 domain separator. + function eip712DomainSeparator() external view returns (bytes32); + + /// @notice Returns the assessor imageId and its url. + /// @return The imageId and its url. + function imageInfo() external view returns (bytes32, string memory); + + /// Returns the address of the token used for collateral deposits. + // forge-lint: disable-next-item(mixed-case-function) + function COLLATERAL_TOKEN_CONTRACT() external view returns (address); +} diff --git a/contracts/src/legacy/LEGACY-FROZEN.md b/contracts/src/legacy/LEGACY-FROZEN.md new file mode 100644 index 000000000..56810d319 --- /dev/null +++ b/contracts/src/legacy/LEGACY-FROZEN.md @@ -0,0 +1,110 @@ +# `contracts/src/legacy/` — frozen audited tree + +This subtree is a frozen copy of `BoundlessMarket` and its transitive +dependencies as deployed on Base mainnet. It exists so the new market can +forward its pre-router legacy ABI to the audited bytecode at the existing +implementation address via a `fallback() + delegatecall` shim, without +re-introducing the legacy bodies into the new market's bytecode. + +## Provenance + +The sources here mirror **`main` at commit +[`507f7469`](https://github.com/boundless-xyz/boundless/commit/507f7469)** +(`BM-2598: add depositCollateralTo and depositCollateralWithPermitTo`, +2026-03-13) — the last commit on `main` that touched any of the files in +this tree. The only diffs from that commit are the file-basename + import +renames documented in the table below; the contract bodies are identical. + +To verify provenance for any file in this tree: + +```bash +diff <(git show 507f7469:contracts/src/) \ + contracts/src/legacy/ +``` + +(For types/libraries that don't reference the renamed interfaces, the +diff should be empty.) + +The on-chain identity that ultimately matters is the deployed bytecode at +the BoundlessMarket proxy's pre-upgrade implementation address. On Base +mainnet that is `0x22bb6bbe5d221ef3e738029dab4d1d27ec725cd3`. The +bytecode-parity invariant under `contracts/test/legacy/deployed-bytecode.hex` + +`deployed-bytecode.meta.toml` is the load-bearing check, regardless of +which git commit the source provenance points at. + +## Architecture + +``` + ┌──────────────────────────────────────┐ + │ Proxy (BoundlessMarket, address P) │ + │ delegate-calls active impl │ + └──────────────┬───────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────┐ +│ NEW market impl (src/BoundlessMarket.sol) │ +│ │ +│ • declared selectors run here: │ +│ lockRequest, slash, withdraw, │ +│ submitRequest, deposit*, every view │ +│ getter shared with legacy, and the │ +│ new-shape fulfill(FulfillmentBatch[]) │ +│ │ +│ • everything else falls through: │ +│ fallback() → delegatecall(LEGACY_IMPL) │ +└──────────────────────┬───────────────────────┘ + │ msg.sender, msg.value, + │ proxy storage all preserved + ▼ +┌──────────────────────────────────────────────┐ +│ LEGACY impl (src/legacy/ │ +│ BoundlessMarketLegacy.sol) │ +│ │ +│ Audited deployed bytecode at the pre- │ +│ upgrade implementation address (Base │ +│ mainnet: 0x22bb...cd3). │ +│ │ +│ Reads + writes the same storage slots the │ +│ new market does (requestLocks at slot 0, │ +│ accounts at slot 1, imageUrl at slot 2). │ +└──────────────────────────────────────────────┘ +``` + +## What's in here + +| Path | Role | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `BoundlessMarketLegacy.sol` | Frozen copy of `main`'s `BoundlessMarket`. Renamed file basename only; the contract symbol stays `BoundlessMarket` so deployedBytecode matches the audited deployment byte-for-byte. | +| `IBoundlessMarketLegacy.sol` | Frozen `IBoundlessMarket` interface (defines the legacy `Fulfillment[] + AssessorReceipt` shape, `imageInfo`, `verifyDelivery`, etc.). | +| `IBoundlessMarketCallbackLegacy.sol` | Frozen callback interface. | +| `libraries/{BoundlessMarketLib,MerkleProofish}.sol` | Frozen library deps. | +| `types/*.sol` | Frozen type tree (`Account`, `RequestLock`, `Fulfillment` with `id`+`requestDigest`, `AssessorReceipt`, etc.) the legacy contract was deployed against. | + +File basenames are suffixed with `Legacy` so that forge writes artifacts to +distinct `out/` directories from the equivalents in `src/`. **Contract and +interface symbols are deliberately unchanged**: that preserves the +`bytecode_hash = none` build's byte-identical match against the deployed +audited code. + +## Freeze policy + +**Do not modify any file in this tree.** + +The CI job `legacy-bytecode-parity` (in `.github/workflows/contracts.yml`) +runs `contracts/scripts/verify-legacy-bytecode.py`, which fails any PR whose +`legacy/` source no longer compiles to a byte-identical match of +`contracts/test/legacy/deployed-bytecode.hex` (the snapshot of the deployed +OLD impl) after masking the constructor-immutable byte positions. The +expected immutable values themselves are also re-checked against +`contracts/test/legacy/deployed-bytecode.meta.toml`. + +## Storage layout interop + +The `legacy-bytecode-parity` job also runs +`contracts/scripts/verify-storage-layout.py`, which asserts that every +storage slot reachable from both `src/BoundlessMarket` and +`src/legacy/BoundlessMarketLegacy` has the same layout (label, slot, +offset, normalized type, plus identical struct member layouts for `Account` +and `RequestLock`). If you're adding or modifying a struct in +`src/types/`, that script will catch any divergence from the legacy view +before it can corrupt delegate-call interop. diff --git a/contracts/src/legacy/libraries/BoundlessMarketLib.sol b/contracts/src/legacy/libraries/BoundlessMarketLib.sol new file mode 100644 index 000000000..412618f02 --- /dev/null +++ b/contracts/src/legacy/libraries/BoundlessMarketLib.sol @@ -0,0 +1,35 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. + +pragma solidity ^0.8.26; + +import {IRiscZeroVerifier} from "risc0/IRiscZeroVerifier.sol"; + +library BoundlessMarketLib { + string constant EIP712_DOMAIN = "IBoundlessMarket"; + string constant EIP712_DOMAIN_VERSION = "1"; + + /// @notice ABI encode the constructor args for this contract. + /// @dev This function exists to provide a type-safe way to ABI-encode constructor args, for + /// use in the deployment process with OpenZeppelin Upgrades. Must be kept in sync with the + /// signature of the BoundlessMarket constructor. + function encodeConstructorArgs( + IRiscZeroVerifier verifier, + IRiscZeroVerifier applicationVerifier, + bytes32 assessorId, + bytes32 deprecatedAssessorId, + uint32 deprecatedAssessorDuration, + address stakeTokenContract + ) internal pure returns (bytes memory) { + return abi.encode( + verifier, + applicationVerifier, + assessorId, + deprecatedAssessorId, + deprecatedAssessorDuration, + stakeTokenContract + ); + } +} diff --git a/contracts/src/legacy/libraries/MerkleProofish.sol b/contracts/src/legacy/libraries/MerkleProofish.sol new file mode 100644 index 000000000..eb2c10c07 --- /dev/null +++ b/contracts/src/legacy/libraries/MerkleProofish.sol @@ -0,0 +1,64 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {IBoundlessMarket} from "../IBoundlessMarketLegacy.sol"; + +library MerkleProofish { + // Compute the root of the Merkle tree given all of its leaves. + // Assumes that the array of leaves is no longer needed, and can be overwritten. + function processTree(bytes32[] memory leaves) internal pure returns (bytes32 root) { + if (leaves.length == 0) { + revert IBoundlessMarket.InvalidRequest(); + } + + // If there's only one leaf, the root is the leaf itself + if (leaves.length == 1) { + return leaves[0]; + } + + uint256 n = leaves.length; + + // Process the leaves array in pairs, iteratively computing the hash of each pair + while (n > 1) { + uint256 nextLevelLength = (n + 1) / 2; // Upper bound of next level (handles odd number of elements) + + // Hash the current level's pairs and place results at the start of the array + for (uint256 i = 0; i < n / 2; i++) { + leaves[i] = _hashPair(leaves[2 * i], leaves[2 * i + 1]); + } + + // If there's an odd number of elements, propagate the last element directly + if (n % 2 == 1) { + leaves[nextLevelLength - 1] = leaves[n - 1]; + } + + // Move to the next level (the computed hashes are now the new "leaves") + n = nextLevelLength; + } + + // The root is now the single element left in the array + root = leaves[0]; + } + + /** + * @dev Sorts the pair (a, b) and hashes the result. + */ + function _hashPair(bytes32 a, bytes32 b) internal pure returns (bytes32) { + return a < b ? _efficientHash(a, b) : _efficientHash(b, a); + } + + /** + * @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory. + */ + function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, a) + mstore(0x20, b) + value := keccak256(0x00, 0x40) + } + } +} diff --git a/contracts/src/legacy/types/Account.sol b/contracts/src/legacy/types/Account.sol new file mode 100644 index 000000000..b01d96878 --- /dev/null +++ b/contracts/src/legacy/types/Account.sol @@ -0,0 +1,87 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +uint256 constant REQUEST_FLAGS_BITWIDTH = 2; +uint256 constant REQUEST_FLAGS_INITIAL_BITS = 64; + +using AccountLibrary for Account global; + +/// @title Account Struct and Library +/// @notice Represents the account state, including balance and request flags. +struct Account { + /// @notice The balance of the account. + /// @dev uint96 is enough to represent the entire token supply of Ether. + uint96 balance; + /// @dev Balance of collateral tokens. + uint96 collateralBalance; + /// @notice 32 pairs of 2 bits representing the status of a request. One bit is for lock-in and + /// the other is for fulfillment. + /// @dev Request state flags are packed into a uint64 to make balance and flags for the first + /// 32 requests fit in one slot. + uint64 requestFlagsInitial; + /// @dev Flags for the remaining requests are in a storage array. + /// Each uint256 holds the packed flags for 128 requests, indexed in a linear fashion. + /// Note that this struct cannot be instantiated in memory. + uint256[(1 << 32) * REQUEST_FLAGS_BITWIDTH / 256] requestFlagsExtended; +} + +library AccountLibrary { + /// @notice Gets the locked and fulfilled request flags for the request with the given index. + /// @param self The account to get the request flags from. + /// @param idx The index of the request. + /// @return locked True if the request is locked, false otherwise. + /// @return fulfilled True if the request is fulfilled, false otherwise. + // forge-lint: disable-next-item(incorrect-shift) + function requestFlags(Account storage self, uint32 idx) internal view returns (bool locked, bool fulfilled) { + if (idx < REQUEST_FLAGS_INITIAL_BITS / REQUEST_FLAGS_BITWIDTH) { + uint64 masked = + (self.requestFlagsInitial + & (uint64((1 << REQUEST_FLAGS_BITWIDTH) - 1) << uint64(idx * REQUEST_FLAGS_BITWIDTH))) + >> (idx * REQUEST_FLAGS_BITWIDTH); + return (masked & uint64(1) != 0, masked & uint64(2) != 0); + } else { + uint256 idxShifted = idx - (REQUEST_FLAGS_INITIAL_BITS / REQUEST_FLAGS_BITWIDTH); + uint256 packed = self.requestFlagsExtended[(idxShifted * REQUEST_FLAGS_BITWIDTH) / 256]; + uint256 maskShift = (idxShifted * REQUEST_FLAGS_BITWIDTH) % 256; + uint256 masked = (packed & (uint256((1 << REQUEST_FLAGS_BITWIDTH) - 1) << maskShift)) >> maskShift; + return (masked & uint256(1) != 0, masked & uint256(2) != 0); + } + } + + /// @notice Sets the locked and fulfilled request flags for the request with the given index. + /// @dev The given value of flags will be applied with |= to the flags for the request. Least significant bit is locked, second-least significant is fulfilled. + /// @param self The account to set the request flags for. + /// @param idx The index of the request. + /// @param flags The flags to set for the request. + // forge-lint: disable-next-item(incorrect-shift) + function setRequestFlags(Account storage self, uint32 idx, uint8 flags) internal { + assert(flags < (1 << REQUEST_FLAGS_BITWIDTH)); + if (idx < REQUEST_FLAGS_INITIAL_BITS / REQUEST_FLAGS_BITWIDTH) { + uint64 mask = uint64(flags) << uint64(idx * REQUEST_FLAGS_BITWIDTH); + self.requestFlagsInitial |= mask; + } else { + uint256 idxShifted = idx - (REQUEST_FLAGS_INITIAL_BITS / REQUEST_FLAGS_BITWIDTH); + uint256 mask = uint256(flags) << (uint256(idxShifted * REQUEST_FLAGS_BITWIDTH) % 256); + self.requestFlagsExtended[(idxShifted * REQUEST_FLAGS_BITWIDTH) / 256] |= mask; + } + } + + /// @notice Sets the locked flag for the request with the given index. + /// @dev The flag indicates that a request has been locked now or in the past. + /// If a requests lock expires this flag will still be set. + /// @param self The account to set the request flag for. + /// @param idx The index of the request. + function setRequestLocked(Account storage self, uint32 idx) internal { + setRequestFlags(self, idx, 1); + } + + /// @notice Sets the fulfilled flag for the request with the given index. + /// @param self The account to set the request flag for. + /// @param idx The index of the request. + function setRequestFulfilled(Account storage self, uint32 idx) internal { + setRequestFlags(self, idx, 2); + } +} diff --git a/contracts/src/legacy/types/AssessorCallback.sol b/contracts/src/legacy/types/AssessorCallback.sol new file mode 100644 index 000000000..7033abd12 --- /dev/null +++ b/contracts/src/legacy/types/AssessorCallback.sol @@ -0,0 +1,14 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +struct AssessorCallback { + /// @notice The index of the fill in the request + uint16 index; + /// @notice The address of the contract to call back + address addr; + /// @notice Maximum gas to use for the callback + uint96 gasLimit; +} diff --git a/contracts/src/legacy/types/AssessorCommitment.sol b/contracts/src/legacy/types/AssessorCommitment.sol new file mode 100644 index 000000000..40405546b --- /dev/null +++ b/contracts/src/legacy/types/AssessorCommitment.sol @@ -0,0 +1,47 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {RequestId} from "./RequestId.sol"; + +using AssessorCommitmentLibrary for AssessorCommitment global; + +/// @title Assessor Commitment Struct +/// @notice Represents the structured commitment used as a leaf in the Assessor guest Merkle tree guest. +struct AssessorCommitment { + /// @notice The index of the request in the tree. + uint256 index; + /// @notice The request ID. + RequestId id; + /// @notice The request digest. + bytes32 requestDigest; + /// @notice The claim digest. + bytes32 claimDigest; + /// @notice The fulfillment data digest. + bytes32 fulfillmentDataDigest; +} + +library AssessorCommitmentLibrary { + /// @dev Id is uint256 as for user defined types, the eip712 type hash uses the underlying type. + string constant ASSESSOR_COMMITMENT_TYPE = + "AssessorCommitment(uint256 index,uint256 id,bytes32 requestDigest,bytes32 claimDigest,bytes32 fulfillmentDataDigest)"; + bytes32 constant ASSESSOR_COMMITMENT_TYPEHASH = keccak256(bytes(ASSESSOR_COMMITMENT_TYPE)); + + /// @notice Computes the EIP-712 digest for the given commitment. + /// @param commitment The commitment to compute the digest for. + /// @return The EIP-712 digest of the commitment. + function eip712Digest(AssessorCommitment memory commitment) internal pure returns (bytes32) { + return keccak256( + abi.encode( + ASSESSOR_COMMITMENT_TYPEHASH, + commitment.index, + commitment.id, + commitment.requestDigest, + commitment.claimDigest, + commitment.fulfillmentDataDigest + ) + ); + } +} diff --git a/contracts/src/legacy/types/AssessorJournal.sol b/contracts/src/legacy/types/AssessorJournal.sol new file mode 100644 index 000000000..48724d318 --- /dev/null +++ b/contracts/src/legacy/types/AssessorJournal.sol @@ -0,0 +1,25 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {AssessorCallback} from "./AssessorCallback.sol"; +import {Selector} from "./Selector.sol"; + +/// @title Assessor Journal Struct +/// @notice Represents the structured journal of the Assessor guest which verifies the signature(s) +/// from client(s) and that the requirements are met by claim digest(s) in the Merkle tree committed +/// to by the given root. +struct AssessorJournal { + /// @notice The (optional) callbacks for the requests committed by the assessor. + AssessorCallback[] callbacks; + /// @notice The (optional) selectors for the requests committed by the assessor. + /// @dev This is used to verify the fulfillment of the request against its selector's seal. + Selector[] selectors; + /// @notice Root of the Merkle tree committing to the set of proven claims. + /// @dev In the case of a batch of size one, this may simply be the eip712Digest of the `AssessorCommitment`. + bytes32 root; + /// @notice The address of the prover that produced the assessor receipt. + address prover; +} diff --git a/contracts/src/legacy/types/AssessorReceipt.sol b/contracts/src/legacy/types/AssessorReceipt.sol new file mode 100644 index 000000000..6d71a6360 --- /dev/null +++ b/contracts/src/legacy/types/AssessorReceipt.sol @@ -0,0 +1,22 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {AssessorCallback} from "./AssessorCallback.sol"; +import {Selector} from "./Selector.sol"; + +/// @title AssessorReceipt Struct and Library +/// @notice Represents the output of the assessor and proof of correctness, allowing request fulfillment. +struct AssessorReceipt { + /// @notice Cryptographic proof for the validity of the execution results. + /// @dev This will be sent to the `IRiscZeroVerifier` associated with this contract. + bytes seal; + /// @notice Optional callbacks committed into the journal. + AssessorCallback[] callbacks; + /// @notice Optional selectors committed into the journal. + Selector[] selectors; + /// @notice Address of the prover + address prover; +} diff --git a/contracts/src/legacy/types/Callback.sol b/contracts/src/legacy/types/Callback.sol new file mode 100644 index 000000000..6478a8f8f --- /dev/null +++ b/contracts/src/legacy/types/Callback.sol @@ -0,0 +1,28 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +using CallbackLibrary for Callback global; + +/// @title Callback Struct and Library +/// @notice Represents a callback configuration for proof delivery +struct Callback { + /// @notice The address of the contract to call back + address addr; + /// @notice Maximum gas to use for the callback + uint96 gasLimit; +} + +library CallbackLibrary { + string constant CALLBACK_TYPE = "Callback(address addr,uint96 gasLimit)"; + bytes32 constant CALLBACK_TYPEHASH = keccak256(bytes(CALLBACK_TYPE)); + + /// @notice Computes the EIP-712 digest for the given callback + /// @param callback The callback to compute the digest for + /// @return The EIP-712 digest of the callback + function eip712Digest(Callback memory callback) internal pure returns (bytes32) { + return keccak256(abi.encode(CALLBACK_TYPEHASH, callback.addr, callback.gasLimit)); + } +} diff --git a/contracts/src/legacy/types/Fulfillment.sol b/contracts/src/legacy/types/Fulfillment.sol new file mode 100644 index 000000000..0e6aacf11 --- /dev/null +++ b/contracts/src/legacy/types/Fulfillment.sol @@ -0,0 +1,37 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {RequestId} from "./RequestId.sol"; +import {FulfillmentDataType} from "./FulfillmentData.sol"; + +using FulfillmentLibrary for Fulfillment global; + +/// @title Fulfillment Struct and Library +/// @notice Represents the information posted by the prover to fulfill a request and get paid. +struct Fulfillment { + /// @notice ID of the request that is being fulfilled. + RequestId id; + /// @notice EIP-712 digest of request struct. + bytes32 requestDigest; + /// @notice Claim Digest + bytes32 claimDigest; + /// @notice The type of data included in the fulfillment + FulfillmentDataType fulfillmentDataType; + /// @notice The fulfillment data + bytes fulfillmentData; + /// @notice Cryptographic proof for the validity of the execution results. + /// @dev This will be sent to the `IRiscZeroVerifier` associated with this contract. + bytes seal; +} + +library FulfillmentLibrary { + /// @notice Computes the digest of the fulfillment data that is committed to by the assessor. + /// @param fulfillment The Fulfillment struct containing potentially the journal + /// @return The keccak256 digest of the fulfillmentData. + function fulfillmentDataDigest(Fulfillment memory fulfillment) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(uint8(fulfillment.fulfillmentDataType), fulfillment.fulfillmentData)); + } +} diff --git a/contracts/src/legacy/types/FulfillmentContext.sol b/contracts/src/legacy/types/FulfillmentContext.sol new file mode 100644 index 000000000..5a5bb78bd --- /dev/null +++ b/contracts/src/legacy/types/FulfillmentContext.sol @@ -0,0 +1,62 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +using FulfillmentContextLibrary for FulfillmentContext global; + +/// @title FulfillmentContext +/// @notice A struct for storing validated fulfillment information in transient storage +/// @dev This struct is designed to be packed into a single uint256 for efficient transient storage +struct FulfillmentContext { + /// @notice Boolean set to true to indicate the request is internally consistent and signed. + bool valid; + /// @notice Boolean set to true to indicate that the request is expired. + bool expired; + /// @notice The validated price for the request + uint96 price; +} + +library FulfillmentContextLibrary { + uint256 private constant VALID_MASK = 1 << 127; + uint256 private constant EXPIRED_MASK = 1 << 126; + uint256 private constant PRICE_MASK = (1 << 96) - 1; + + /// @notice Packs the struct into a single 256-bit slots and sets the flags. + /// @param x The FulfillmentContext struct to pack + /// @return Packed uint256 containing valid bit and price + function pack(FulfillmentContext memory x) internal pure returns (uint256) { + return (x.valid ? VALID_MASK : 0) | (x.expired ? EXPIRED_MASK : 0) | uint256(x.price); + } + + /// @notice Unpacks the struct from a single uint256 + /// @param packed Packed uint256 containing the flags and price + /// @return The unpacked FulfillmentContext struct + function unpack(uint256 packed) internal pure returns (FulfillmentContext memory) { + return FulfillmentContext({ + valid: (packed & VALID_MASK) != 0, expired: (packed & EXPIRED_MASK) != 0, price: uint96(packed & PRICE_MASK) + }); + } + + /// @notice Packs and stores the object to transient storage + /// @param x The FulfillmentContext struct to store + /// @param requestDigest The storage key for the transient storage + function store(FulfillmentContext memory x, bytes32 requestDigest) internal { + uint256 packed = pack(x); + assembly { + tstore(requestDigest, packed) + } + } + + /// @notice Loads from transient storage and unpacks to FulfillmentContext + /// @param requestDigest The storage key to load from + /// @return The loaded and unpacked FulfillmentContext struct + function load(bytes32 requestDigest) internal view returns (FulfillmentContext memory) { + uint256 packed; + assembly { + packed := tload(requestDigest) + } + return unpack(packed); + } +} diff --git a/contracts/src/legacy/types/FulfillmentData.sol b/contracts/src/legacy/types/FulfillmentData.sol new file mode 100644 index 000000000..56f4b7959 --- /dev/null +++ b/contracts/src/legacy/types/FulfillmentData.sol @@ -0,0 +1,55 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +using FulfillmentDataLibrary for FulfillmentDataImageIdAndJournal global; + +enum FulfillmentDataType { + None, + ImageIdAndJournal +} + +/// @title FulfillmentDataImageIdAndJournal Struct and Library +/// @notice Represents a fulfillment where the image id and journal are delivered +struct FulfillmentDataImageIdAndJournal { + /// @notice Image ID of the guest that was verifiably executed to satisfy the request. + bytes32 imageId; + /// @notice Journal committed by the guest program execution. + bytes journal; +} + +library FulfillmentDataLibrary { + /// @notice Decodes a bytes calldata into a FulfillmentDataImageIdAndJournal struct. + /// @param data The bytes calldata to decode. + /// @return fillData The decoded FulfillmentDataImageIdAndJournal struct. + function decodeFulfillmentDataImageIdAndJournal(bytes calldata data) + public + pure + returns (FulfillmentDataImageIdAndJournal memory fillData) + { + (fillData.imageId, fillData.journal) = decodePackedImageIdAndJournal(data); + } + + /// @notice Decodes a bytes calldata into a the image id and journal. + /// @param data The bytes calldata to decode. + /// @return imageId The decoded image ID. + /// @return journal The decoded journal. + function decodePackedImageIdAndJournal(bytes calldata data) + internal + pure + returns (bytes32 imageId, bytes calldata journal) + { + assembly { + // Extract imageId (first 32 bytes after length) + imageId := calldataload(add(data.offset, 0x20)) + // Extract journal offset and create calldata slice + let journalOffset := calldataload(add(data.offset, 0x40)) + let journalPtr := add(data.offset, add(0x20, journalOffset)) + let journalLength := calldataload(journalPtr) + journal.offset := add(journalPtr, 0x20) + journal.length := journalLength + } + } +} diff --git a/contracts/src/legacy/types/Input.sol b/contracts/src/legacy/types/Input.sol new file mode 100644 index 000000000..7c560ba32 --- /dev/null +++ b/contracts/src/legacy/types/Input.sol @@ -0,0 +1,46 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +using InputLibrary for Input global; + +/// @title Input Types and Library +/// @notice Provides functions to create and handle different types of inputs. +enum InputType { + Inline, + Url +} + +/// @notice Represents an input with a type and data. +struct Input { + InputType inputType; + bytes data; +} + +library InputLibrary { + string constant INPUT_TYPE = "Input(uint8 inputType,bytes data)"; + bytes32 constant INPUT_TYPEHASH = keccak256(bytes(INPUT_TYPE)); + + /// @notice Creates an inline input. + /// @param inlineData The data for the inline input. + /// @return An Input struct with type Inline and the provided data. + function createInlineInput(bytes memory inlineData) internal pure returns (Input memory) { + return Input({inputType: InputType.Inline, data: inlineData}); + } + + /// @notice Creates a URL input. + /// @param url The URL for the input. + /// @return An Input struct with type Url and the provided URL as data. + function createUrlInput(string memory url) internal pure returns (Input memory) { + return Input({inputType: InputType.Url, data: bytes(url)}); + } + + /// @notice Computes the EIP-712 digest for the given input. + /// @param input The input to compute the digest for. + /// @return The EIP-712 digest of the input. + function eip712Digest(Input memory input) internal pure returns (bytes32) { + return keccak256(abi.encode(INPUT_TYPEHASH, input.inputType, keccak256(input.data))); + } +} diff --git a/contracts/src/legacy/types/LockRequest.sol b/contracts/src/legacy/types/LockRequest.sol new file mode 100644 index 000000000..54db0f557 --- /dev/null +++ b/contracts/src/legacy/types/LockRequest.sol @@ -0,0 +1,52 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {ProofRequest, ProofRequestLibrary} from "./ProofRequest.sol"; +import {CallbackLibrary} from "./Callback.sol"; +import {OfferLibrary} from "./Offer.sol"; +import {PredicateLibrary} from "./Predicate.sol"; +import {InputLibrary} from "./Input.sol"; +import {RequirementsLibrary} from "./Requirements.sol"; + +using LockRequestLibrary for LockRequest global; + +/// @title Lock Request Struct and Library +/// @notice Message sent by a prover to indicate that they intend to lock the given request. +struct LockRequest { + /// @notice The proof request that the prover is locking. + ProofRequest request; +} + +library LockRequestLibrary { + string constant LOCK_REQUEST_TYPE = "LockRequest(ProofRequest request)"; + + bytes32 constant LOCK_REQUEST_TYPEHASH = keccak256( + abi.encodePacked( + LOCK_REQUEST_TYPE, + CallbackLibrary.CALLBACK_TYPE, + InputLibrary.INPUT_TYPE, + OfferLibrary.OFFER_TYPE, + PredicateLibrary.PREDICATE_TYPE, + ProofRequestLibrary.PROOF_REQUEST_TYPE, + RequirementsLibrary.REQUIREMENTS_TYPE + ) + ); + + /// @notice Computes the EIP-712 digest for the given lock request. + /// @param lockRequest The lock request to compute the digest for. + /// @return The EIP-712 digest of the lock request. + function eip712Digest(LockRequest memory lockRequest) internal pure returns (bytes32) { + return keccak256(abi.encode(LOCK_REQUEST_TYPEHASH, lockRequest.request.eip712Digest())); + } + + /// @notice Computes the EIP-712 digest for the given lock request from a precomputed EIP-712 proof request digest. + /// @dev This avoids recomputing the proof request digest in the case where the proof request digest has already been computed. + /// @param proofRequestEip712Digest The EIP-712 digest of the proof request. + /// @return The EIP-712 digest of the lock request. + function eip712DigestFromPrecomputedDigest(bytes32 proofRequestEip712Digest) internal pure returns (bytes32) { + return keccak256(abi.encode(LOCK_REQUEST_TYPEHASH, proofRequestEip712Digest)); + } +} diff --git a/contracts/src/legacy/types/Offer.sol b/contracts/src/legacy/types/Offer.sol new file mode 100644 index 000000000..547a09eff --- /dev/null +++ b/contracts/src/legacy/types/Offer.sol @@ -0,0 +1,164 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {IBoundlessMarket} from "../IBoundlessMarketLegacy.sol"; + +using OfferLibrary for Offer global; + +/// @title Offer Struct and Library +/// @notice Represents an offer and provides functions to validate and compute offer-related data. +struct Offer { + /// @notice Price at the start of the bidding period, it is minimum price a prover will receive for job. + uint256 minPrice; + /// @notice Price at the end of the bidding period, this is the maximum price the client will pay. + uint256 maxPrice; + /// @notice Time at which the ramp-up period starts, in seconds since the UNIX epoch. + uint64 rampUpStart; + /// @notice Length of the "ramp-up period," measured in seconds since bidding start. + /// @dev Once bidding starts, the price begins to "ramp-up." During this time, the price rises + /// each block until it reaches `maxPrice. + uint32 rampUpPeriod; + /// @notice Timeout for the lock, expressed as seconds from ramp up start. + /// @dev Once locked, if a valid proof is not submitted before this deadline, the prover can + /// be "slashed", which refunds the price to the requester and takes the prover stake. + /// + /// Additionally, the fee paid by the client is zero for proofs delivered after this time. + /// Note that after this time, and before `timeout` a proof can still be delivered to fulfill + /// the request. This applies both to locked and unlocked requests; if a proof is delivered + /// after this timeout, no fee will be paid from the client. + uint32 lockTimeout; + /// @notice Timeout for the request, expressed as seconds from ramp up start. + /// @dev After this time the request is considered completely expired and can no longer be + /// fulfilled. After this time, the `slash` action can be completed to finalize the transaction + /// if it was locked but not fulfilled. + uint32 timeout; + /// @notice Bidders must provide this amount of collateral as part of their bid. + uint256 lockCollateral; +} + +library OfferLibrary { + using SafeCast for uint256; + + string constant OFFER_TYPE = + "Offer(uint256 minPrice,uint256 maxPrice,uint64 rampUpStart,uint32 rampUpPeriod,uint32 lockTimeout,uint32 timeout,uint256 lockCollateral)"; + bytes32 constant OFFER_TYPEHASH = keccak256(abi.encodePacked(OFFER_TYPE)); + + /// @notice Validates that price, ramp-up, timeout, and deadline are internally consistent and well formed. + /// @param offer The offer to validate. + /// @return lockDeadline1 The deadline for when a lock expires for the offer. + /// @return deadline1 The deadline for the offer as a whole. + function validate(Offer memory offer) internal pure returns (uint64 lockDeadline1, uint64 deadline1) { + if (offer.minPrice > offer.maxPrice) { + revert IBoundlessMarket.InvalidRequest(); + } + if (offer.rampUpPeriod > offer.lockTimeout) { + revert IBoundlessMarket.InvalidRequest(); + } + if (offer.lockTimeout > offer.timeout) { + revert IBoundlessMarket.InvalidRequest(); + } + lockDeadline1 = offer.lockDeadline(); + deadline1 = offer.deadline(); + if (deadline1 - lockDeadline1 > type(uint24).max) { + revert IBoundlessMarket.InvalidRequest(); + } + } + + /// @notice Calculates the earliest time at which the offer will be worth at least the given price. + /// @dev Returned time will always be in the range 0 to offer.rampUpStart + offer.rampUpPeriod. + /// @param offer The offer to calculate for. + /// @param price The price to calculate the time for. + /// @return The earliest time at which the offer will be worth at least the given price. + function timeAtPrice(Offer memory offer, uint256 price) internal pure returns (uint64) { + if (price > offer.maxPrice) { + revert IBoundlessMarket.InvalidRequest(); + } + + if (price <= offer.minPrice) { + return 0; + } + + // Note: If we are in this branch, then + // offer.minPrice < offer.maxPrice + // This means it is safe to divide by the difference + + uint256 rise = uint256(offer.maxPrice - offer.minPrice); + uint256 run = uint256(offer.rampUpPeriod); + + uint256 delta = Math.ceilDiv(uint256(price - offer.minPrice) * run, rise); + return offer.rampUpStart + delta.toUint64(); + } + + /// @notice Calculates the price at the given time. + /// @dev Price increases linearly during the ramp-up period, then remains at the max price until + /// the lock deadline. After the lock deadline, the price goes to zero. As a result, provers are + /// paid no fee from the client for requests that are fulfilled after lock deadline. Note though + /// that there may be a reward of stake available, if a prover failed to deliver on the request. + /// @param offer The offer to calculate for. + /// @param timestamp The time to calculate the price for, as a UNIX timestamp. + /// @return The price at the given time. + function priceAt(Offer memory offer, uint64 timestamp) internal pure returns (uint256) { + if (timestamp <= offer.rampUpStart) { + return offer.minPrice; + } + + if (timestamp > offer.lockDeadline()) { + return 0; + } + + if (timestamp <= offer.rampUpStart + offer.rampUpPeriod) { + // Note: if we are in this branch, then 0 < offer.rampUpPeriod + // This means it is safe to divide by offer.rampUpPeriod + + uint256 rise = uint256(offer.maxPrice - offer.minPrice); + uint256 run = uint256(offer.rampUpPeriod); + uint256 delta = timestamp - uint256(offer.rampUpStart); + + // Note: delta <= run + // This means (delta * rise) / run <= rise + // This means price <= offer.maxPrice + + uint256 price = uint256(offer.minPrice) + (delta * rise) / run; + return price; + } + + return offer.maxPrice; + } + + /// @notice Calculates the deadline for the offer. + /// @param offer The offer to calculate the deadline for. + /// @return The deadline for the offer, as a UNIX timestamp. + function deadline(Offer memory offer) internal pure returns (uint64) { + return offer.rampUpStart + offer.timeout; + } + + /// @notice Calculates the lock deadline for the offer. + /// @param offer The offer to calculate the lock deadline for. + /// @return The lock deadline for the offer, as a UNIX timestamp. + function lockDeadline(Offer memory offer) internal pure returns (uint64) { + return offer.rampUpStart + offer.lockTimeout; + } + + /// @notice Computes the EIP-712 digest for the given offer. + /// @param offer The offer to compute the digest for. + /// @return The EIP-712 digest of the offer. + function eip712Digest(Offer memory offer) internal pure returns (bytes32) { + return keccak256( + abi.encode( + OFFER_TYPEHASH, + offer.minPrice, + offer.maxPrice, + offer.rampUpStart, + offer.rampUpPeriod, + offer.lockTimeout, + offer.timeout, + offer.lockCollateral + ) + ); + } +} diff --git a/contracts/src/legacy/types/Predicate.sol b/contracts/src/legacy/types/Predicate.sol new file mode 100644 index 000000000..6fa9027cb --- /dev/null +++ b/contracts/src/legacy/types/Predicate.sol @@ -0,0 +1,121 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.26; + +import {ReceiptClaim, ReceiptClaimLib} from "risc0/IRiscZeroVerifier.sol"; +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; + +using PredicateLibrary for Predicate global; +using ReceiptClaimLib for ReceiptClaim; + +/// @title Predicate Struct and Library +/// @notice A predicate is a function over the claim that determines whether it meets the clients requirements. +/// The data field is used to store the specific data associated with the predicate. +/// - DigestMatch: (bytes32, bytes32) -> abi.encodePacked(imageId, journalHash) +/// - PrefixMatch: (bytes32, bytes) -> abi.encodePacked(imageId, prefix) +/// - ClaimDigestMatch: (bytes32) -> abi.encode(claimDigest) +struct Predicate { + PredicateType predicateType; + bytes data; +} + +enum PredicateType { + DigestMatch, + PrefixMatch, + ClaimDigestMatch +} + +library PredicateLibrary { + string constant PREDICATE_TYPE = "Predicate(uint8 predicateType,bytes data)"; + bytes32 constant PREDICATE_TYPEHASH = keccak256(bytes(PREDICATE_TYPE)); + + /// @notice Creates a digest match predicate. + /// @param hash The hash to match. + /// @return A Predicate struct with type DigestMatch and the provided hash. + function createDigestMatchPredicate(bytes32 imageId, bytes32 hash) internal pure returns (Predicate memory) { + return Predicate({predicateType: PredicateType.DigestMatch, data: abi.encodePacked(imageId, hash)}); + } + + /// @notice Creates a prefix match predicate. + /// @param prefix The prefix to match. + /// @return A Predicate struct with type PrefixMatch and the provided prefix. + function createPrefixMatchPredicate(bytes32 imageId, bytes memory prefix) internal pure returns (Predicate memory) { + return Predicate({predicateType: PredicateType.PrefixMatch, data: abi.encodePacked(imageId, prefix)}); + } + + /// @notice Creates a claim digest match predicate. + /// @param claimDigest The claimDigest to match. + /// @return A Predicate struct with type ClaimDigestMatch and the provided claimDigest. + function createClaimDigestMatchPredicate(bytes32 claimDigest) internal pure returns (Predicate memory) { + return Predicate({predicateType: PredicateType.ClaimDigestMatch, data: abi.encodePacked(claimDigest)}); + } + + /// @notice Evaluates the predicate against the image ID and journal. + /// @dev If the predicate is of type ClaimDigestMatch and image ID and journal are not available, + /// use the evaluation function with the claim digest instead. + /// @param predicate The predicate to evaluate. + /// @param imageId Image ID to use for evaluation. + /// @param journal The journal to evaluate against. + /// @return True if the predicate is satisfied, false otherwise. + function eval(Predicate memory predicate, bytes32 imageId, bytes memory journal) internal pure returns (bool) { + if (predicate.predicateType == PredicateType.DigestMatch) { + require(predicate.data.length == 64, "Invalid DigestMatch data length"); + bytes memory dataJournal = Bytes.slice(predicate.data, 32); + return bytes32(dataJournal) == sha256(abi.encode(journal)) && bytes32(predicate.data) == imageId; + } else if (predicate.predicateType == PredicateType.PrefixMatch) { + require(predicate.data.length >= 32, "Invalid PrefixMatch data length"); + bytes memory dataJournal = Bytes.slice(predicate.data, 32); + return startsWith(journal, dataJournal) && bytes32(predicate.data) == imageId; + } else if (predicate.predicateType == PredicateType.ClaimDigestMatch) { + require(predicate.data.length == 32, "Invalid ClaimDigestMatch data length"); + return bytes32(predicate.data) == ReceiptClaimLib.ok(imageId, sha256(abi.encode(journal))).digest(); + } else { + revert("Unreachable code"); + } + } + + /// @notice Evaluates the predicate against the claim digest. + /// @dev This function should be used when the predicate is of type ClaimDigestMatch + /// and the image ID and journal are not available. + /// @param predicate The predicate to evaluate. + /// @param claimDigest Claim digest to use for evaluation. + /// @return True if the predicate is satisfied, false otherwise. + function eval(Predicate memory predicate, bytes32 claimDigest) internal pure returns (bool) { + if (predicate.predicateType == PredicateType.ClaimDigestMatch) { + require(predicate.data.length == 32, "Invalid ClaimDigestMatch data length"); + return bytes32(predicate.data) == claimDigest; + } else { + revert("Predicate not of type ClaimDigestMatch"); + } + } + + /// @notice Checks if the journal starts with the given prefix. + /// @param journal The journal to check. + /// @param prefix The prefix to check for. + /// @return True if the journal starts with the prefix, false otherwise. + function startsWith(bytes memory journal, bytes memory prefix) internal pure returns (bool) { + if (journal.length < prefix.length) { + return false; + } + if (prefix.length == 0) { + return true; + } + bytes memory slice = new bytes(prefix.length); + assembly { + let dest := add(slice, 0x20) + let src := add(journal, 0x20) + for { let i := 0 } lt(i, mload(prefix)) { i := add(i, 0x20) } { mstore(add(dest, i), mload(add(src, i))) } + } + return keccak256(slice) == keccak256(prefix); + } + + /// @notice Computes the EIP-712 digest for the given predicate. + /// @param predicate The predicate to compute the digest for. + /// @return The EIP-712 digest of the predicate. + function eip712Digest(Predicate memory predicate) internal pure returns (bytes32) { + return keccak256(abi.encode(PREDICATE_TYPEHASH, predicate.predicateType, keccak256(predicate.data))); + } +} diff --git a/contracts/src/legacy/types/ProofRequest.sol b/contracts/src/legacy/types/ProofRequest.sol new file mode 100644 index 000000000..9cb50d991 --- /dev/null +++ b/contracts/src/legacy/types/ProofRequest.sol @@ -0,0 +1,74 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {RequestId} from "./RequestId.sol"; +import {CallbackLibrary} from "./Callback.sol"; +import {Offer, OfferLibrary} from "./Offer.sol"; +import {PredicateLibrary} from "./Predicate.sol"; +import {Input, InputLibrary} from "./Input.sol"; +import {Requirements, RequirementsLibrary} from "./Requirements.sol"; + +using ProofRequestLibrary for ProofRequest global; + +/// @title Proof Request Struct and Library +/// @notice Represents a proof request with its associated data and functions. +struct ProofRequest { + /// @notice Unique ID for this request, constructed from the client address and a 32-bit index. + RequestId id; + /// @notice Requirements of the delivered proof. + /// @dev Specifies the program that must be run, constrains the value of the journal, and specifies a callback required to be called when the proof is delivered. + Requirements requirements; + /// @notice A public URI where the program (i.e. image) can be downloaded. + /// @dev This URI will be accessed by provers that are evaluating whether to bid on the request. + string imageUrl; + /// @notice Input to be provided to the zkVM guest execution. + Input input; + /// @notice Offer specifying how much the client is willing to pay to have this request fulfilled. + Offer offer; +} + +library ProofRequestLibrary { + /// @dev Id is uint256 as for user defined types, the eip712 type hash uses the underlying type. + string constant PROOF_REQUEST_TYPE = + "ProofRequest(uint256 id,Requirements requirements,string imageUrl,Input input,Offer offer)"; + + bytes32 constant PROOF_REQUEST_TYPEHASH = keccak256( + abi.encodePacked( + PROOF_REQUEST_TYPE, + CallbackLibrary.CALLBACK_TYPE, + InputLibrary.INPUT_TYPE, + OfferLibrary.OFFER_TYPE, + PredicateLibrary.PREDICATE_TYPE, + RequirementsLibrary.REQUIREMENTS_TYPE + ) + ); + + /// @notice Computes the EIP-712 digest for the given proof request. + /// @param request The proof request to compute the digest for. + /// @return The EIP-712 digest of the proof request. + function eip712Digest(ProofRequest memory request) internal pure returns (bytes32) { + return keccak256( + abi.encode( + PROOF_REQUEST_TYPEHASH, + request.id, + request.requirements.eip712Digest(), + keccak256(bytes(request.imageUrl)), + request.input.eip712Digest(), + request.offer.eip712Digest() + ) + ); + } + + /// @notice Validates the proof request with the intention for it to be priced. + /// Does not check if the request is already locked or fulfilled, but does check + /// if it has expired. + /// @param request The proof request to validate. + /// @return lockDeadline The deadline for when a lock expires for the request. + /// @return deadline The deadline for the request as a whole. + function validate(ProofRequest calldata request) internal pure returns (uint64 lockDeadline, uint64 deadline) { + return request.offer.validate(); + } +} diff --git a/contracts/src/legacy/types/RequestId.sol b/contracts/src/legacy/types/RequestId.sol new file mode 100644 index 000000000..09e611409 --- /dev/null +++ b/contracts/src/legacy/types/RequestId.sol @@ -0,0 +1,68 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {IBoundlessMarket} from "../IBoundlessMarketLegacy.sol"; + +type RequestId is uint256; + +using RequestIdLibrary for RequestId global; + +library RequestIdLibrary { + uint256 internal constant SMART_CONTRACT_SIGNATURE_FLAG = 1 << 192; + + /// @notice Creates a RequestId from a client address and a 32-bit index. + /// @param client1 The address of the client. + /// @param id The 32-bit index. + /// @return The constructed RequestId. + function from(address client1, uint32 id) internal pure returns (RequestId) { + return RequestId.wrap(uint256(uint160(client1)) << 32 | uint256(id)); + } + + /// @notice Creates a RequestId from a client address, a 32-bit index, and a smart contract signature flag. + /// @param client1 The address of the client. + /// @param id The 32-bit index. + /// @param isSmartContractSig Whether the request uses a smart contract signature. + /// @return The constructed RequestId. + function from(address client1, uint32 id, bool isSmartContractSig) internal pure returns (RequestId) { + uint256 encoded = uint256(uint160(client1)) << 32 | uint256(id); + if (isSmartContractSig) { + encoded = encoded | SMART_CONTRACT_SIGNATURE_FLAG; + } + return RequestId.wrap(encoded); + } + + /// @notice Extracts the client address and index from a RequestId. + /// @param id The RequestId to extract from. + /// @return The client address and the 32-bit index. + function clientAndIndex(RequestId id) internal pure returns (address, uint32) { + uint256 unwrapped = RequestId.unwrap(id); + if (unwrapped & (type(uint256).max << 193) != 0) { + revert IBoundlessMarket.InvalidRequest(); + } + return (address(uint160(unwrapped >> 32)), uint32(unwrapped)); + } + + /// @notice Extracts the client address and index from a RequestId. + /// @param id The RequestId to extract from. + /// @return The client address and the 32-bit index, and true if the signature is a smart contract signature. + function clientIndexAndSignatureType(RequestId id) internal pure returns (address, uint32, bool) { + uint256 unwrapped = RequestId.unwrap(id); + if (unwrapped & (type(uint256).max << 193) != 0) { + revert IBoundlessMarket.InvalidRequest(); + } + return (address(uint160(unwrapped >> 32)), uint32(unwrapped), (unwrapped & SMART_CONTRACT_SIGNATURE_FLAG) != 0); + } + + function client(RequestId id) internal pure returns (address) { + uint256 unwrapped = RequestId.unwrap(id); + return address(uint160(unwrapped >> 32)); + } + + function isSmartContractSigned(RequestId id) internal pure returns (bool) { + uint256 unwrapped = RequestId.unwrap(id); + return (unwrapped & SMART_CONTRACT_SIGNATURE_FLAG) != 0; + } +} diff --git a/contracts/src/legacy/types/RequestLock.sol b/contracts/src/legacy/types/RequestLock.sol new file mode 100644 index 000000000..218c2bf4a --- /dev/null +++ b/contracts/src/legacy/types/RequestLock.sol @@ -0,0 +1,122 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +using RequestLockLibrary for RequestLock global; + +/// @notice Stores information about requests that have been locked. +/// @dev RequestLock is an internal structure that is modified at various points in the proof lifecycle. +/// Fields can be valid or invalid depending where in the lifecycle we are. Integrators should not rely on RequestLock +/// for determining the status of a request. Instead, they should always use BoundlessMarket's public functions. +/// +/// Packed to fit into 3 slots. +struct RequestLock { + /// + /// Storage slot 0 + /// + /// @notice The address of the prover that locked the request _or_ the address of the prover that fulfilled the request. + address prover; + /// @notice The final timestamp at which the locked request can be fulfilled for payment by the locker. + uint64 lockDeadline; + /// @notice The number of seconds from the lockDeadline to where the request expires. + /// @dev Represented as a delta so that it can be packed into 2 slots. + uint24 deadlineDelta; + /// @notice Flags that indicate the state of the request lock. + uint8 requestLockFlags; + /// + /// Storage slots 1 + /// + /// @notice The price that the prover will be paid for fulfilling the request. + uint96 price; + // Prover collateral that may be taken if a proof is not delivered by the deadline. + uint96 collateral; + /// + /// Storage slot 2 + /// + /// @notice Keccak256 hash of the request. During fulfillment, this value is used + /// to check that the request completed is the request that was locked, and not some other + /// request with the same ID. + /// @dev This digest binds the full request including e.g. the offer and input. Technically, + /// all that is required is to bind the requirements. If there is some advantage to only binding + /// the requirements here (e.g. less hashing costs) then that might be worth doing. + /// + /// There is another option here, which would be to have the request lock mapping index + /// based on request digest instead of index. As a friction, this would introduce a second + /// user-facing concept of what identifies a request. + bytes32 requestDigest; +} + +library RequestLockLibrary { + uint8 internal constant PROVER_PAID_DURING_LOCK_FLAG = 1 << 0; + uint8 internal constant PROVER_PAID_AFTER_LOCK_FLAG = 1 << 1; + uint8 internal constant SLASHED_FLAG = 1 << 2; + + /// @notice Calculates the deadline for the locked request. + /// @param requestLock The request lock to calculate the deadline for. + /// @return The deadline for the request. + function deadline(RequestLock memory requestLock) internal pure returns (uint64) { + return requestLock.lockDeadline + requestLock.deadlineDelta; + } + + function setProverPaidBeforeLockDeadline(RequestLock storage requestLock) internal { + requestLock.requestLockFlags = PROVER_PAID_DURING_LOCK_FLAG; + // Zero out slots 1 for gas refund. Slot 1 is only required for slashing. + // Slot 2 is required to support a single request having multiple proofs delivered. + clearSlot1(requestLock); + } + + function setProverPaidAfterLockDeadline(RequestLock storage requestLock, address prover) internal { + requestLock.prover = prover; + requestLock.requestLockFlags |= PROVER_PAID_AFTER_LOCK_FLAG; + // We don't zero out any slots as slot 1 is required for slashing, and slot 2 is required + // to support a single request having multiple proofs delivered. + } + + function setSlashed(RequestLock storage requestLock) internal { + requestLock.requestLockFlags |= SLASHED_FLAG; + // Zero out slots 1 for gas refund. Slot 2 is required to support partial fulfillment after + // the request has expired. + clearSlot1(requestLock); + } + + /// @notice Returns true if the request was fulfilled by the locker + /// before the lock deadline and they have been paid. + /// @param requestLock The request lock to check. + /// @return True if the request was fulfilled before the lock deadline and the prover was paid, false otherwise. + function isProverPaidBeforeLockDeadline(RequestLock memory requestLock) internal pure returns (bool) { + return requestLock.requestLockFlags & PROVER_PAID_DURING_LOCK_FLAG != 0; + } + + /// @notice Checks if the request was fulfilled by any prover after the lock deadline. + /// @param requestLock The request lock to check. + /// @return True if the request is fulfilled after the lock deadline and the prover was paid, false otherwise. + function isProverPaidAfterLockDeadline(RequestLock memory requestLock) internal pure returns (bool) { + return requestLock.requestLockFlags & PROVER_PAID_AFTER_LOCK_FLAG != 0; + } + + /// @notice Checks if the locked request was fulfilled and _a_ prover was paid. The prover paid + /// could be the prover that locked, or a prover that filled after the lock deadline. + /// @param requestLock The request lock to check. + /// @return True if the request is fulfilled after the lock deadline, false otherwise. + function isProverPaid(RequestLock memory requestLock) internal pure returns (bool) { + return isProverPaidBeforeLockDeadline(requestLock) || isProverPaidAfterLockDeadline(requestLock); + } + + /// @notice Checks if the request was slashed. + /// @dev Whether a request resulted in a slash does not indicate whether the request was fulfilled + /// since it is possible for a request to be fulfilled after a request lock has expired. + /// @param requestLock The request lock to check. + /// @return True if the request is slashed, false otherwise. + function isSlashed(RequestLock memory requestLock) internal pure returns (bool) { + return requestLock.requestLockFlags & SLASHED_FLAG != 0; + } + + function clearSlot1(RequestLock storage requestLock) private { + assembly { + let num := add(requestLock.slot, 1) + sstore(num, 0) + } + } +} diff --git a/contracts/src/legacy/types/Requirements.sol b/contracts/src/legacy/types/Requirements.sol new file mode 100644 index 000000000..0f86f69ed --- /dev/null +++ b/contracts/src/legacy/types/Requirements.sol @@ -0,0 +1,36 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {Predicate, PredicateLibrary} from "./Predicate.sol"; +import {Callback, CallbackLibrary} from "./Callback.sol"; + +using RequirementsLibrary for Requirements global; + +struct Requirements { + Callback callback; + Predicate predicate; + bytes4 selector; +} + +library RequirementsLibrary { + string constant REQUIREMENTS_TYPE = "Requirements(Callback callback,Predicate predicate,bytes4 selector)"; + bytes32 constant REQUIREMENTS_TYPEHASH = + keccak256(abi.encodePacked(REQUIREMENTS_TYPE, CallbackLibrary.CALLBACK_TYPE, PredicateLibrary.PREDICATE_TYPE)); + + // @notice Computes the EIP-712 digest of the requirements + // @param requirements The requirements to digest + // @return The EIP-712 digest of the requirements + function eip712Digest(Requirements memory requirements) internal pure returns (bytes32) { + return keccak256( + abi.encode( + REQUIREMENTS_TYPEHASH, + CallbackLibrary.eip712Digest(requirements.callback), + PredicateLibrary.eip712Digest(requirements.predicate), + requirements.selector + ) + ); + } +} diff --git a/contracts/src/legacy/types/Selector.sol b/contracts/src/legacy/types/Selector.sol new file mode 100644 index 000000000..7e3eb0196 --- /dev/null +++ b/contracts/src/legacy/types/Selector.sol @@ -0,0 +1,14 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +/// @title Selector - A representation of the bytes4 selector and its index within a batch. +/// @dev This is only used as part of the AssessorJournal and AssessorReceipt. +struct Selector { + /// @notice Index within a batch where the selector is required. + uint16 index; + /// @notice The actual required selector. + bytes4 value; +} diff --git a/contracts/src/libraries/BoundlessMarketLib.sol b/contracts/src/libraries/BoundlessMarketLib.sol index d05e30fa5..0ffadcd56 100644 --- a/contracts/src/libraries/BoundlessMarketLib.sol +++ b/contracts/src/libraries/BoundlessMarketLib.sol @@ -15,11 +15,11 @@ library BoundlessMarketLib { /// @dev This function exists to provide a type-safe way to ABI-encode constructor args, for /// use in the deployment process with OpenZeppelin Upgrades. Must be kept in sync with the /// signature of the BoundlessMarket constructor. - function encodeConstructorArgs(BoundlessRouter router, address stakeTokenContract) + function encodeConstructorArgs(BoundlessRouter router, address stakeTokenContract, address legacyImpl) internal pure returns (bytes memory) { - return abi.encode(router, stakeTokenContract); + return abi.encode(router, stakeTokenContract, legacyImpl); } } diff --git a/contracts/test/BoundlessMarket.t.sol b/contracts/test/BoundlessMarket.t.sol index 1a4710e8e..8f96bd7c6 100644 --- a/contracts/test/BoundlessMarket.t.sol +++ b/contracts/test/BoundlessMarket.t.sol @@ -73,8 +73,10 @@ bytes32 constant APP_IMAGE_ID = 0x0000000000000000000000000000000000000000000000 bytes32 constant APP_IMAGE_ID_2 = 0x0000000000000000000000000000000000000000000000000000000000000002; bytes32 constant SET_BUILDER_IMAGE_ID = 0x0000000000000000000000000000000000000000000000000000000000000002; bytes32 constant ASSESSOR_IMAGE_ID = 0x0000000000000000000000000000000000000000000000000000000000000003; -bytes32 constant DEPRECATED_ASSESSOR_IMAGE_ID = 0x0000000000000000000000000000000000000000000000000000000000000004; -uint32 constant DEPRECATED_ASSESSOR_DURATION = 1 minutes; +// Non-functional legacy-impl placeholder. The new market's constructor rejects the zero address, +// but the new ABI never routes through the legacy fallback, so tests point legacyImpl at a dead +// address rather than a real BoundlessMarketLegacy. Fallback coverage lives in test/legacy/. +address constant LEGACY_IMPL_STUB = 0xdEDEDEDEdEdEdEDedEDeDedEdEdeDedEdEDedEdE; bytes constant APP_JOURNAL = bytes("GUEST JOURNAL"); bytes constant APP_JOURNAL_2 = bytes("GUEST JOURNAL 2"); @@ -99,6 +101,7 @@ contract BoundlessMarketTest is Test { R0BoundlessAssessorAdapter internal r0AssessorAdapter; address internal boundlessMarketSource; + address internal legacyImpl; address internal proxy; RiscZeroSetVerifier internal setVerifier; HitPoints internal collateralToken; @@ -203,12 +206,17 @@ contract BoundlessMarketTest is Test { setVerifierAdapter = new R0BoundlessVerifierAdapter(setVerifier); router.instantiate(setVerifier.SELECTOR(), address(setVerifierAdapter), VERIFIER_CLASS_ID, 0); + // The new ABI never routes through the legacy fallback, so the new market's + // delegate-call target is a non-functional stub rather than a real + // BoundlessMarketLegacy. Fallback compatibility is covered in test/legacy/. + legacyImpl = LEGACY_IMPL_STUB; + // Deploy the UUPS proxy with the implementation - boundlessMarketSource = address(new BoundlessMarket(router, address(collateralToken))); + boundlessMarketSource = address(new BoundlessMarket(router, address(collateralToken), legacyImpl)); proxy = UnsafeUpgrades.deployUUPSProxy( boundlessMarketSource, abi.encodeCall(BoundlessMarket.initialize, (ownerWallet.addr)) ); - boundlessMarket = BoundlessMarket(proxy); + boundlessMarket = BoundlessMarket(payable(proxy)); mockCallback = new MockCallback(setVerifier, address(boundlessMarket), APP_IMAGE_ID, 10_000); mockHighGasCallback = new MockCallback(setVerifier, address(boundlessMarket), APP_IMAGE_ID, 250_000); @@ -4523,17 +4531,17 @@ contract BoundlessMarketUpgradeTest is BoundlessMarketTest { function testUnsafeUpgrade() public { vm.startPrank(ownerWallet.addr); proxy = UnsafeUpgrades.deployUUPSProxy( - address(new BoundlessMarket(router, address(collateralToken))), + address(new BoundlessMarket(router, address(collateralToken), legacyImpl)), abi.encodeCall(BoundlessMarket.initialize, (ownerWallet.addr)) ); - boundlessMarket = BoundlessMarket(proxy); + boundlessMarket = BoundlessMarket(payable(proxy)); address implAddressV1 = UnsafeUpgrades.getImplementationAddress(proxy); // Should emit an `Upgraded` event vm.expectEmit(false, true, true, true); emit IERC1967.Upgraded(address(0)); UnsafeUpgrades.upgradeProxy( - proxy, address(new BoundlessMarket(router, address(collateralToken))), "", ownerWallet.addr + proxy, address(new BoundlessMarket(router, address(collateralToken), legacyImpl)), "", ownerWallet.addr ); vm.stopPrank(); address implAddressV2 = UnsafeUpgrades.getImplementationAddress(proxy); diff --git a/contracts/test/legacy/BoundlessMarketLegacy.t.sol b/contracts/test/legacy/BoundlessMarketLegacy.t.sol new file mode 100644 index 000000000..48e4bacde --- /dev/null +++ b/contracts/test/legacy/BoundlessMarketLegacy.t.sol @@ -0,0 +1,4394 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. + +pragma solidity ^0.8.26; + +import {console} from "forge-std/console.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; +import { + IRiscZeroVerifier, + ReceiptClaim, + Receipt as RiscZeroReceipt, + ReceiptClaimLib, + VerificationFailed +} from "risc0/IRiscZeroVerifier.sol"; +import {RiscZeroMockVerifier} from "risc0/test/RiscZeroMockVerifier.sol"; +import {TestUtils} from "./TestUtils.sol"; +import {Client} from "./clients/Client.sol"; +import {IERC1967} from "@openzeppelin/contracts/interfaces/IERC1967.sol"; +import {UnsafeUpgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {HitPoints} from "../../src/HitPoints.sol"; + +import {BoundlessMarket} from "../../src/legacy/BoundlessMarketLegacy.sol"; +import {Callback} from "../../src/legacy/types/Callback.sol"; +import { + FulfillmentDataImageIdAndJournal, + FulfillmentDataLibrary, + FulfillmentDataType +} from "../../src/legacy/types/FulfillmentData.sol"; +import {RequestId} from "../../src/legacy/types/RequestId.sol"; +import {AssessorCallback} from "../../src/legacy/types/AssessorCallback.sol"; +import {BoundlessMarketLib} from "../../src/legacy/libraries/BoundlessMarketLib.sol"; +import {MerkleProofish} from "../../src/legacy/libraries/MerkleProofish.sol"; +import {ProofRequest} from "../../src/legacy/types/ProofRequest.sol"; +import {LockRequest} from "../../src/legacy/types/LockRequest.sol"; +import {Fulfillment} from "../../src/legacy/types/Fulfillment.sol"; +import {AssessorReceipt} from "../../src/legacy/types/AssessorReceipt.sol"; +import {Offer} from "../../src/legacy/types/Offer.sol"; +import {Requirements} from "../../src/legacy/types/Requirements.sol"; +import {Predicate, PredicateLibrary, PredicateType} from "../../src/legacy/types/Predicate.sol"; +import {IBoundlessMarket} from "../../src/legacy/IBoundlessMarketLegacy.sol"; + +import {RiscZeroSetVerifier} from "risc0/RiscZeroSetVerifier.sol"; +import {Fulfillment} from "../../src/legacy/types/Fulfillment.sol"; +import {MockCallback} from "./MockCallback.sol"; +import {Selector} from "../../src/legacy/types/Selector.sol"; + +import {SmartContractClient} from "./clients/SmartContractClient.sol"; +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; + +Vm constant VM = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +bytes32 constant APP_IMAGE_ID = 0x0000000000000000000000000000000000000000000000000000000000000001; +bytes32 constant APP_IMAGE_ID_2 = 0x0000000000000000000000000000000000000000000000000000000000000002; +bytes32 constant SET_BUILDER_IMAGE_ID = 0x0000000000000000000000000000000000000000000000000000000000000002; +bytes32 constant ASSESSOR_IMAGE_ID = 0x0000000000000000000000000000000000000000000000000000000000000003; +bytes32 constant DEPRECATED_ASSESSOR_IMAGE_ID = 0x0000000000000000000000000000000000000000000000000000000000000004; +uint32 constant DEPRECATED_ASSESSOR_DURATION = 1 minutes; + +bytes constant APP_JOURNAL = bytes("GUEST JOURNAL"); +bytes constant APP_JOURNAL_2 = bytes("GUEST JOURNAL 2"); + +contract BoundlessMarketLegacyTest is Test { + using ReceiptClaimLib for ReceiptClaim; + using BoundlessMarketLib for Requirements; + using BoundlessMarketLib for ProofRequest; + using BoundlessMarketLib for Offer; + using TestUtils for RiscZeroSetVerifier; + using TestUtils for Selector[]; + using TestUtils for AssessorCallback[]; + using SafeCast for uint256; + using SafeCast for int256; + + RiscZeroMockVerifier internal verifier; + BoundlessMarket internal boundlessMarket; + + address internal boundlessMarketSource; + address internal proxy; + RiscZeroSetVerifier internal setVerifier; + HitPoints internal collateralToken; + mapping(uint256 => Client) internal clients; + mapping(uint256 => Client) internal provers; + mapping(uint256 => SmartContractClient) internal smartContractClients; + Client internal testProver; + address internal testProverAddress; + uint256 initialBalance; + int256 internal stakeBalanceSnapshot; + int256 internal collateralTreasuryBalanceSnapshot; + + uint256 constant DEFAULT_BALANCE = 1000 ether; + uint256 constant EXPECTED_DEFAULT_MAX_GAS_FOR_VERIFY = 50000; + uint256 constant EXPECTED_SLASH_BURN_BPS = 5000; + + ReceiptClaim internal appClaim = ReceiptClaimLib.ok(APP_IMAGE_ID, sha256(APP_JOURNAL)); + + Vm.Wallet internal ownerWallet = vm.createWallet("OWNER"); + + MockCallback internal mockCallback; + MockCallback internal mockHighGasCallback; + + function setUp() public { + vm.deal(ownerWallet.addr, DEFAULT_BALANCE); + + vm.startPrank(ownerWallet.addr); + + // Deploy the implementation contracts + verifier = new RiscZeroMockVerifier(bytes4(0)); + setVerifier = new RiscZeroSetVerifier(verifier, SET_BUILDER_IMAGE_ID, "https://set-builder.dev.null"); + collateralToken = new HitPoints(ownerWallet.addr); + + // Deploy the market under test. Overridable so the via-fallback suite can + // deploy the new market in front of this legacy impl and exercise the same + // battery through BoundlessMarket.fallback() (see + // BoundlessMarketLegacyViaFallback.t.sol). + _deployMarket(); + + // Initialize MockCallbacks + mockCallback = new MockCallback(setVerifier, address(boundlessMarket), APP_IMAGE_ID, 10_000); + mockHighGasCallback = new MockCallback(setVerifier, address(boundlessMarket), APP_IMAGE_ID, 250_000); + + collateralToken.grantMinterRole(ownerWallet.addr); + collateralToken.grantAuthorizedTransferRole(proxy); + vm.stopPrank(); + + testProver = getProver(1); + testProverAddress = testProver.addr(); + for (uint256 i = 0; i < 5; i++) { + getClient(i); + getProver(i); + getSmartContractClient(i); + } + + initialBalance = address(boundlessMarket).balance; + + stakeBalanceSnapshot = type(int256).max; + collateralTreasuryBalanceSnapshot = type(int256).max; + + // Verify that OWNER has the admin role + assertTrue( + boundlessMarket.hasRole(boundlessMarket.ADMIN_ROLE(), ownerWallet.addr), + "OWNER address does not have admin role after deployment" + ); + } + + /// @dev Deploys the market under test behind a UUPS proxy and assigns + /// `boundlessMarketSource`, `proxy`, and `boundlessMarket`. Called from + /// setUp() after the verifier and collateral token are deployed. The + /// via-fallback suite overrides this to deploy the new market in front of + /// the legacy impl; everything else in the suite is shared. + function _deployMarket() internal virtual { + boundlessMarketSource = address( + new BoundlessMarket( + setVerifier, + setVerifier, + ASSESSOR_IMAGE_ID, + DEPRECATED_ASSESSOR_IMAGE_ID, + DEPRECATED_ASSESSOR_DURATION, + address(collateralToken) + ) + ); + proxy = UnsafeUpgrades.deployUUPSProxy( + boundlessMarketSource, + abi.encodeCall(BoundlessMarket.initialize, (ownerWallet.addr, "https://assessor.dev.null")) + ); + boundlessMarket = BoundlessMarket(proxy); + } + + function expectedSlashBurnAmount(uint256 amount) internal pure returns (uint96) { + return uint96((uint256(amount) * EXPECTED_SLASH_BURN_BPS) / 10000); + } + + function expectedSlashTransferAmount(uint256 amount) internal pure returns (uint96) { + return uint96((uint256(amount) * (10000 - EXPECTED_SLASH_BURN_BPS)) / 10000); + } + + function expectMarketBalanceUnchanged() internal view { + uint256 finalBalance = address(boundlessMarket).balance; + console.log("Initial balance:", initialBalance); + console.log("Final balance:", finalBalance); + require(finalBalance == initialBalance, "Market balance changed during the test"); + } + + function snapshotMarketCollateralBalance() public { + stakeBalanceSnapshot = collateralToken.balanceOf(address(boundlessMarket)).toInt256(); + } + + function expectMarketCollateralBalanceChange(int256 change) public view { + require(stakeBalanceSnapshot != type(int256).max, "market stake balance snapshot is not set"); + int256 newBalance = collateralToken.balanceOf(address(boundlessMarket)).toInt256(); + console.log("Market stake balance at block %d: %d", block.number, newBalance.toUint256()); + int256 expectedBalance = stakeBalanceSnapshot + change; + require(expectedBalance >= 0, "expected market stake balance cannot be less than 0"); + console.log("Market expected stake balance at block %d: %d", block.number, expectedBalance.toUint256()); + require(expectedBalance == newBalance, "market stake balance is not equal to expected value"); + } + + function snapshotMarketStakeTreasuryBalance() public { + collateralTreasuryBalanceSnapshot = boundlessMarket.balanceOfCollateral(address(boundlessMarket)).toInt256(); + } + + function expectMarketCollateralTreasuryBalanceChange(int256 change) public view { + require( + collateralTreasuryBalanceSnapshot != type(int256).max, + "market collateral treasury balance snapshot is not set" + ); + int256 newBalance = boundlessMarket.balanceOfCollateral(address(boundlessMarket)).toInt256(); + console.log("Market stake treasury balance at block %d: %d", block.number, newBalance.toUint256()); + int256 expectedBalance = collateralTreasuryBalanceSnapshot + change; + require(expectedBalance >= 0, "expected market treasury stake balance cannot be less than 0"); + console.log("Market expected stake treasury balance at block %d: %d", block.number, expectedBalance.toUint256()); + require(expectedBalance == newBalance, "market stake treasury balance is not equal to expected value"); + } + + function expectRequestFulfilled(RequestId requestId) internal view { + require(boundlessMarket.requestIsFulfilled(requestId), "Request should be fulfilled"); + require(!boundlessMarket.requestIsSlashed(requestId), "Request should not be slashed"); + } + + function expectRequestFulfilledAndSlashed(RequestId requestId) internal view { + require(boundlessMarket.requestIsFulfilled(requestId), "Request should be fulfilled"); + require(boundlessMarket.requestIsSlashed(requestId), "Request should be slashed"); + } + + function expectRequestNotFulfilled(RequestId requestId) internal view { + require(!boundlessMarket.requestIsFulfilled(requestId), "Request should not be fulfilled"); + } + + function expectRequestSlashed(RequestId requestId) internal view { + require(boundlessMarket.requestIsSlashed(requestId), "Request should be slashed"); + } + + function expectRequestNotSlashed(RequestId requestId) internal view { + require(!boundlessMarket.requestIsSlashed(requestId), "Request should be slashed"); + } + + // Creates a client account with the given index, gives it some Ether, + // gives it some Stake Token, and deposits both into the market. + function getClient(uint256 index) internal returns (Client) { + if (address(clients[index]) != address(0)) { + return clients[index]; + } + Client client = createClientContract(string.concat("CLIENT_", vm.toString(index))); + fundClient(client); + clients[index] = client; + return client; + } + + // Creates a client account with the given index, gives it some Ether, + // gives it some Stake Token, and deposits both into the market. + function getSmartContractClient(uint256 index) internal returns (SmartContractClient) { + if (address(smartContractClients[index]) != address(0)) { + return smartContractClients[index]; + } + SmartContractClient client = createSmartContractClientContract(string.concat("SC_CLIENT_", vm.toString(index))); + fundSmartContractClient(client); + smartContractClients[index] = client; + return client; + } + + // Creates a prover account with the given index, gives it some Ether, + // gives it some Stake Token, and deposits both into the market. + function getProver(uint256 index) internal returns (Client) { + if (address(provers[index]) != address(0)) { + return provers[index]; + } + Client prover = createClientContract(string.concat("PROVER_", vm.toString(index))); + fundClient(prover); + provers[index] = prover; + return prover; + } + + function fundClient(Client client) internal { + address clientAddress = client.addr(); + // Deal the client from Ether and deposit it in the market. + vm.deal(clientAddress, DEFAULT_BALANCE); + vm.prank(clientAddress); + boundlessMarket.deposit{value: DEFAULT_BALANCE}(); + + // Snapshot their initial ETH balance. + client.snapshotBalance(); + + // Mint some stake tokens. + vm.prank(ownerWallet.addr); + collateralToken.mint(clientAddress, DEFAULT_BALANCE); + + uint256 deadline = block.timestamp + 1 hours; + (uint8 v, bytes32 r, bytes32 s) = client.signPermit(proxy, DEFAULT_BALANCE, deadline); + vm.prank(clientAddress); + boundlessMarket.depositCollateralWithPermit(DEFAULT_BALANCE, deadline, v, r, s); + + // Snapshot their initial stake balance. + client.snapshotCollateralBalance(); + } + + function fundSmartContractClient(SmartContractClient client) internal { + address walletAddress = client.addr(); + address signerAddress = client.signerAddr(); + + // Deal the SCW some Ether and deposit it in the market. + vm.deal(walletAddress, DEFAULT_BALANCE); + vm.prank(signerAddress); + client.execute( + address(boundlessMarket), + abi.encodeWithSelector(IBoundlessMarket.deposit.selector, DEFAULT_BALANCE), + DEFAULT_BALANCE + ); + + // Snapshot their initial ETH balance. + client.snapshotBalance(); + + // Mint some stake tokens. + vm.prank(ownerWallet.addr); + collateralToken.mint(walletAddress, DEFAULT_BALANCE); + + vm.prank(signerAddress); + client.execute( + address(collateralToken), abi.encodeWithSelector(IERC20.approve.selector, boundlessMarket, DEFAULT_BALANCE) + ); + + vm.prank(signerAddress); + client.execute( + address(boundlessMarket), + abi.encodeWithSelector(IBoundlessMarket.depositCollateral.selector, DEFAULT_BALANCE) + ); + + // check balances + assertEq(boundlessMarket.balanceOf(walletAddress), DEFAULT_BALANCE); + assertEq(boundlessMarket.balanceOfCollateral(walletAddress), DEFAULT_BALANCE); + + // Snapshot their initial stake balance. + client.snapshotCollateralBalance(); + } + + // Create a client, using a trick to set the address equal to the wallet address. + function createClientContract(string memory identifier) internal returns (Client) { + Vm.Wallet memory wallet = vm.createWallet(identifier); + Client client = new Client(wallet); + client.initialize(identifier, boundlessMarket, collateralToken); + return client; + } + + function createSmartContractClientContract(string memory identifier) internal returns (SmartContractClient) { + Vm.Wallet memory signer = vm.createWallet(string.concat(identifier, "_SIGNER")); + SmartContractClient client = new SmartContractClient(signer); + client.initialize(identifier, boundlessMarket, collateralToken); + return client; + } + + function submitRoot(bytes32 root) internal { + boundlessMarket.submitRoot( + address(setVerifier), + root, + verifier.mockProve( + SET_BUILDER_IMAGE_ID, sha256(abi.encodePacked(SET_BUILDER_IMAGE_ID, uint256(1 << 255), root)) + ) + .seal + ); + } + + function createFillAndSubmitRoot(ProofRequest memory request, bytes memory journal, address prover) + internal + returns (Fulfillment memory, AssessorReceipt memory) + { + return createFillAndSubmitRoot(request, journal, prover, FulfillmentDataType.ImageIdAndJournal); + } + + function createFillAndSubmitRoot( + ProofRequest memory request, + bytes memory journal, + address prover, + FulfillmentDataType fillType + ) internal returns (Fulfillment memory, AssessorReceipt memory) { + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes[] memory journals = new bytes[](1); + journals[0] = journal; + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) = + createFillsAndSubmitRoot(requests, journals, prover, fillType); + return (fills[0], assessorReceipt); + } + + function createDeprecatedFillAndSubmitRoot(ProofRequest memory request, bytes memory journal, address prover) + internal + returns (Fulfillment memory, AssessorReceipt memory) + { + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes[] memory journals = new bytes[](1); + journals[0] = journal; + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) = + createDeprecatedFillsAndSubmitRoot(requests, journals, prover); + return (fills[0], assessorReceipt); + } + + function createFillsAndSubmitRoot(ProofRequest[] memory requests, bytes[] memory journals, address prover) + internal + returns (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) + { + return createFillsAndSubmitRoot(requests, journals, prover, FulfillmentDataType.ImageIdAndJournal); + } + + function createFillsAndSubmitRoot( + ProofRequest[] memory requests, + bytes[] memory journals, + address prover, + FulfillmentDataType fillType + ) internal returns (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) { + bytes32 root; + (fills, assessorReceipt, root) = createFills(requests, journals, prover, fillType, ASSESSOR_IMAGE_ID); + // submit the root to the set verifier + submitRoot(root); + return (fills, assessorReceipt); + } + + function createDeprecatedFillsAndSubmitRoot(ProofRequest[] memory requests, bytes[] memory journals, address prover) + internal + returns (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) + { + bytes32 root; + (fills, assessorReceipt, root) = createDeprecatedFills(requests, journals, prover); + // submit the root to the set verifier + submitRoot(root); + return (fills, assessorReceipt); + } + + function createFills( + ProofRequest[] memory requests, + bytes[] memory journals, + address prover, + FulfillmentDataType fillType, + bytes32 assessorImageId + ) internal view returns (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) { + // initialize the fullfillments; one for each request; + // the seal is filled in later, by calling fillInclusionProof + fills = new Fulfillment[](requests.length); + Selector[] memory selectors = new Selector[](0); + AssessorCallback[] memory callbacks = new AssessorCallback[](0); + + for (uint8 i = 0; i < requests.length; i++) { + bytes32 claimDigest; + bytes memory fulfillmentData; + bytes memory journal = journals[i]; + PredicateType predicateType = requests[i].requirements.predicate.predicateType; + bytes32 imageId; + if (predicateType != PredicateType.ClaimDigestMatch) { + imageId = bytesToBytes32(requests[i].requirements.predicate.data); + claimDigest = ReceiptClaimLib.ok(imageId, sha256(journal)).digest(); + } else { + // this is hacky, but for ClaimDigestMatch, the imageId is not known, + // so we just use the APP_IMAGE_ID as the default + imageId = APP_IMAGE_ID; + claimDigest = bytesToBytes32(requests[i].requirements.predicate.data); + } + if (fillType == FulfillmentDataType.ImageIdAndJournal) { + fulfillmentData = abi.encode(FulfillmentDataImageIdAndJournal({imageId: imageId, journal: journal})); + } + Fulfillment memory fill = Fulfillment({ + id: requests[i].id, + requestDigest: MessageHashUtils.toTypedDataHash( + boundlessMarket.eip712DomainSeparator(), requests[i].eip712Digest() + ), + claimDigest: claimDigest, + fulfillmentData: fulfillmentData, + fulfillmentDataType: fillType, + seal: bytes("") + }); + fills[i] = fill; + if (requests[i].requirements.selector != bytes4(0)) { + selectors = selectors.addSelector(i, requests[i].requirements.selector); + } + if (requests[i].requirements.callback.addr != address(0)) { + callbacks = callbacks.addCallback( + AssessorCallback({ + index: i, + gasLimit: requests[i].requirements.callback.gasLimit, + addr: requests[i].requirements.callback.addr + }) + ); + } + } + + // compute the assessor claim + ReceiptClaim memory assessorClaim = TestUtils.mockAssessor(fills, assessorImageId, selectors, callbacks, prover); + // compute the batchRoot of the batch Merkle Tree (without the assessor) + (bytes32 batchRoot, bytes32[][] memory tree) = TestUtils.mockSetBuilder(fills); + + bytes32 assessorLeaf = TestUtils.hashLeaf(assessorClaim.digest()); + root = MerkleProofish._hashPair(batchRoot, assessorLeaf); + + // compute all the inclusion proofs for the fullfillments + TestUtils.fillInclusionProofs(setVerifier, fills, assessorLeaf, tree); + // compute the assessor fill + assessorReceipt = AssessorReceipt({ + seal: TestUtils.mockAssessorSeal(setVerifier, batchRoot), + selectors: selectors, + callbacks: callbacks, + prover: prover + }); + + return (fills, assessorReceipt, root); + } + + function createFills(ProofRequest[] memory requests, bytes[] memory journals, address prover) + internal + view + returns (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) + { + (fills, assessorReceipt, root) = + createFills(requests, journals, prover, FulfillmentDataType.ImageIdAndJournal, ASSESSOR_IMAGE_ID); + } + + function createDeprecatedFills(ProofRequest[] memory requests, bytes[] memory journals, address prover) + internal + view + returns (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) + { + (fills, assessorReceipt, root) = createFills( + requests, journals, prover, FulfillmentDataType.ImageIdAndJournal, DEPRECATED_ASSESSOR_IMAGE_ID + ); + } + + function newBatch(uint256 batchSize) internal returns (ProofRequest[] memory requests, bytes[] memory journals) { + requests = new ProofRequest[](batchSize); + journals = new bytes[](batchSize); + for (uint256 j = 0; j < 5; j++) { + getClient(j); + } + for (uint256 i = 0; i < batchSize; i++) { + Client client = clients[i % 5]; + ProofRequest memory request = client.request(uint32(i / 5)); + bytes memory clientSignature = client.sign(request); + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + requests[i] = request; + journals[i] = APP_JOURNAL; + } + } + + function newBatchWithSelector(uint256 batchSize, bytes4 selector) + internal + returns (ProofRequest[] memory requests, bytes[] memory journals) + { + requests = new ProofRequest[](batchSize); + journals = new bytes[](batchSize); + for (uint256 j = 0; j < 5; j++) { + getClient(j); + } + for (uint256 i = 0; i < batchSize; i++) { + Client client = clients[i % 5]; + ProofRequest memory request = client.request(uint32(i / 5)); + request.requirements.selector = selector; + bytes memory clientSignature = client.sign(request); + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + requests[i] = request; + journals[i] = APP_JOURNAL; + } + } + + function newBatchWithCallback(uint256 batchSize) + internal + returns (ProofRequest[] memory requests, bytes[] memory journals) + { + requests = new ProofRequest[](batchSize); + journals = new bytes[](batchSize); + for (uint256 j = 0; j < 5; j++) { + getClient(j); + } + for (uint256 i = 0; i < batchSize; i++) { + Client client = clients[i % 5]; + ProofRequest memory request = client.request(uint32(i / 5)); + request.requirements.callback.addr = address(mockCallback); + request.requirements.callback.gasLimit = 500_000; + bytes memory clientSignature = client.sign(request); + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + requests[i] = request; + journals[i] = APP_JOURNAL; + } + } + + function bytesToBytes32(bytes memory b) internal pure returns (bytes32) { + bytes32 out; + for (uint256 i = 0; i < 32; i++) { + out |= bytes32(b[i] & 0xFF) >> (i * 8); + } + return out; + } +} + +contract BoundlessMarketLegacyBasicTest is BoundlessMarketLegacyTest { + using ReceiptClaimLib for ReceiptClaim; + using BoundlessMarketLib for Offer; + using BoundlessMarketLib for ProofRequest; + using SafeCast for uint256; + + function _stringEquals(string memory a, string memory b) private pure returns (bool) { + return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); + } + + function testBytecodeSize() public { + vm.snapshotValue("bytecode size proxy", address(proxy).code.length); + vm.snapshotValue("bytecode size implementation", boundlessMarketSource.code.length); + } + + function testDeposit() public { + vm.deal(testProverAddress, 1 ether); + // Deposit funds into the market + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.Deposit(testProverAddress, 1 ether); + vm.prank(testProverAddress); + boundlessMarket.deposit{value: 1 ether}(); + testProver.expectBalanceChange(1 ether); + } + + function testDeposits() public { + address newUser = address(uint160(3)); + vm.deal(newUser, 2 ether); + + // Deposit funds into the market + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.Deposit(newUser, 1 ether); + vm.prank(newUser); + boundlessMarket.deposit{value: 1 ether}(); + vm.snapshotGasLastCall("deposit: first ever deposit"); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.Deposit(newUser, 1 ether); + vm.prank(newUser); + boundlessMarket.deposit{value: 1 ether}(); + vm.snapshotGasLastCall("deposit: second deposit"); + } + + function testDepositTo() public { + vm.deal(testProverAddress, 1 ether); + // Deposit funds into the market + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.Deposit(testProverAddress, 1 ether); + vm.prank(testProverAddress); + boundlessMarket.depositTo{value: 1 ether}(testProverAddress); + testProver.expectBalanceChange(1 ether); + } + + function testDepositsTo() public { + address newUser = address(uint160(3)); + vm.deal(newUser, 2 ether); + + // Deposit funds into the market + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.Deposit(newUser, 1 ether); + vm.prank(newUser); + boundlessMarket.depositTo{value: 1 ether}(newUser); + vm.snapshotGasLastCall("depositTo: first ever deposit"); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.Deposit(newUser, 1 ether); + vm.prank(newUser); + boundlessMarket.depositTo{value: 1 ether}(newUser); + vm.snapshotGasLastCall("depositTo: second deposit"); + } + + function testAdminRoleSetup() public view { + assertTrue( + boundlessMarket.hasRole(boundlessMarket.ADMIN_ROLE(), ownerWallet.addr), "Owner should have admin role" + ); + } + + function testWithdraw() public { + // Deposit funds into the market + vm.deal(testProverAddress, 1 ether); + vm.prank(testProverAddress); + boundlessMarket.deposit{value: 1 ether}(); + + // Withdraw funds from the market + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.Withdrawal(testProverAddress, 1 ether); + vm.prank(testProverAddress); + boundlessMarket.withdraw(1 ether); + expectMarketBalanceUnchanged(); + + // Attempt to withdraw extra funds from the market. + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.InsufficientBalance.selector, testProverAddress)); + vm.prank(testProverAddress); + boundlessMarket.withdraw(DEFAULT_BALANCE + 1); + expectMarketBalanceUnchanged(); + } + + function testWithdrawals() public { + // Deposit funds into the market + vm.deal(testProverAddress, 3 ether); + vm.prank(testProverAddress); + boundlessMarket.deposit{value: 3 ether}(); + + // Withdraw funds from the market + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.Withdrawal(testProverAddress, 1 ether); + vm.prank(testProverAddress); + boundlessMarket.withdraw(1 ether); + vm.snapshotGasLastCall("withdraw: 1 ether"); + + uint256 balance = boundlessMarket.balanceOf(testProverAddress); + vm.prank(testProverAddress); + boundlessMarket.withdraw(balance); + vm.snapshotGasLastCall("withdraw: full balance"); + assertEq(boundlessMarket.balanceOf(testProverAddress), 0); + + // Attempt to withdraw extra funds from the market. + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.InsufficientBalance.selector, testProverAddress)); + vm.prank(testProverAddress); + boundlessMarket.withdraw(DEFAULT_BALANCE + 1); + } + + function testCollateralDeposit() public { + // Mint some tokens + vm.prank(ownerWallet.addr); + collateralToken.mint(testProverAddress, 2); + + // Approve the market to spend the testProver's collateralToken + vm.prank(testProverAddress); + ERC20(address(collateralToken)).approve(address(boundlessMarket), 2); + vm.snapshotGasLastCall("ERC20 approve: required for depositCollateral"); + + // Deposit stake into the market + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.CollateralDeposit(testProverAddress, 1); + vm.prank(testProverAddress); + boundlessMarket.depositCollateral(1); + vm.snapshotGasLastCall("depositCollateral: 1 HP (tops up market account)"); + testProver.expectCollateralBalanceChange(1); + + // Deposit stake into the market + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.CollateralDeposit(testProverAddress, 1); + vm.prank(testProverAddress); + boundlessMarket.depositCollateral(1); + vm.snapshotGasLastCall("depositCollateral: full (drains testProver account)"); + testProver.expectCollateralBalanceChange(2); + } + + function testCollateralDepositWithPermit() public { + // Mint some tokens + vm.prank(ownerWallet.addr); + collateralToken.mint(testProverAddress, 2); + + // Approve the market to spend the testProver's collateralToken + uint256 deadline = block.timestamp + 1 hours; + (uint8 v, bytes32 r, bytes32 s) = testProver.signPermit(address(boundlessMarket), 1, deadline); + + // Deposit stake into the market + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.CollateralDeposit(testProverAddress, 1); + vm.prank(testProverAddress); + boundlessMarket.depositCollateralWithPermit(1, deadline, v, r, s); + vm.snapshotGasLastCall("depositCollateralWithPermit: 1 HP (tops up market account)"); + testProver.expectCollateralBalanceChange(1); + + // Approve the market to spend the testProver's collateralToken + (v, r, s) = testProver.signPermit(address(boundlessMarket), 1, deadline); + + // Deposit stake into the market + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.CollateralDeposit(testProverAddress, 1); + vm.prank(testProverAddress); + boundlessMarket.depositCollateralWithPermit(1, deadline, v, r, s); + vm.snapshotGasLastCall("depositCollateralWithPermit: full (drains testProver account)"); + testProver.expectCollateralBalanceChange(2); + } + + function testCollateralDepositTo() public { + Client sender = getClient(2); + Client receiver = getClient(3); + address senderAddr = sender.addr(); + address receiverAddr = receiver.addr(); + + vm.prank(ownerWallet.addr); + collateralToken.mint(senderAddr, 2); + + vm.prank(senderAddr); + ERC20(address(collateralToken)).approve(address(boundlessMarket), 2); + + uint256 senderBalanceBefore = boundlessMarket.balanceOfCollateral(senderAddr); + uint256 receiverBalanceBefore = boundlessMarket.balanceOfCollateral(receiverAddr); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.CollateralDeposit(receiverAddr, 1); + vm.prank(senderAddr); + boundlessMarket.depositCollateralTo(receiverAddr, 1); + + assertEq(boundlessMarket.balanceOfCollateral(senderAddr), senderBalanceBefore); + assertEq(boundlessMarket.balanceOfCollateral(receiverAddr), receiverBalanceBefore + 1); + } + + function testCollateralDepositWithPermitTo() public { + Client sender = getClient(2); + Client receiver = getClient(3); + address senderAddr = sender.addr(); + address receiverAddr = receiver.addr(); + + vm.prank(ownerWallet.addr); + collateralToken.mint(senderAddr, 2); + + uint256 deadline = block.timestamp + 1 hours; + (uint8 v, bytes32 r, bytes32 s) = sender.signPermit(address(boundlessMarket), 1, deadline); + + uint256 senderBalanceBefore = boundlessMarket.balanceOfCollateral(senderAddr); + uint256 receiverBalanceBefore = boundlessMarket.balanceOfCollateral(receiverAddr); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.CollateralDeposit(receiverAddr, 1); + vm.prank(senderAddr); + boundlessMarket.depositCollateralWithPermitTo(receiverAddr, 1, deadline, v, r, s); + + assertEq(boundlessMarket.balanceOfCollateral(senderAddr), senderBalanceBefore); + assertEq(boundlessMarket.balanceOfCollateral(receiverAddr), receiverBalanceBefore + 1); + } + + function testStakeWithdraw() public { + // Withdraw stake from the market + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.CollateralWithdrawal(testProverAddress, 1); + vm.prank(testProverAddress); + boundlessMarket.withdrawCollateral(1); + vm.snapshotGasLastCall("withdrawCollateral: 1 HP balance"); + testProver.expectCollateralBalanceChange(-1); + assertEq(collateralToken.balanceOf(testProverAddress), 1, "TestProver should have 1 hitPoint after withdrawing"); + + // Withdraw full stake from the market + uint256 remainingBalance = boundlessMarket.balanceOfCollateral(testProverAddress); + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.CollateralWithdrawal(testProverAddress, remainingBalance); + vm.prank(testProverAddress); + boundlessMarket.withdrawCollateral(remainingBalance); + vm.snapshotGasLastCall("withdrawCollateral: full balance"); + testProver.expectCollateralBalanceChange(-int256(DEFAULT_BALANCE)); + assertEq( + collateralToken.balanceOf(testProverAddress), + DEFAULT_BALANCE, + "TestProver should have DEFAULT_BALANCE hitPoint after withdrawing" + ); + + // Attempt to withdraw extra funds from the market. + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.InsufficientBalance.selector, testProverAddress)); + vm.prank(testProverAddress); + boundlessMarket.withdrawCollateral(1); + } + + function testSubmitRequest() public { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + bytes memory clientSignature = client.sign(request); + + // Submit the request with no funds + // Expect the event to be emitted + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestSubmitted(request.id, request, clientSignature); + boundlessMarket.submitRequest(request, clientSignature); + vm.snapshotGasLastCall("submitRequest: without ether"); + + // Submit the request with funds + // Expect the event to be emitted + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.Deposit(client.addr(), uint256(request.offer.maxPrice)); + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestSubmitted(request.id, request, clientSignature); + vm.deal(client.addr(), request.offer.maxPrice); + address clientAddress = client.addr(); + vm.prank(clientAddress); + boundlessMarket.submitRequest{value: request.offer.maxPrice}(request, clientSignature); + vm.snapshotGasLastCall("submitRequest: with maxPrice ether"); + } + + function _testLockRequest(bool withSig) private returns (Client, ProofRequest memory) { + return _testLockRequest(withSig, ""); + } + + function _testLockRequest(bool withSig, string memory snapshot) private returns (Client, ProofRequest memory) { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + bytes memory clientSignature = client.sign(request); + bytes memory proverSignature = testProver.signLockRequest(LockRequest({request: request})); + + // Expect the event to be emitted + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestLocked(request.id, testProverAddress, request, clientSignature); + if (withSig) { + boundlessMarket.lockRequestWithSignature(request, clientSignature, proverSignature); + } else { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } + + if (!_stringEquals(snapshot, "")) { + vm.snapshotGasLastCall(snapshot); + } + + // Ensure the balances are correct + client.expectBalanceChange(-1 ether); + testProver.expectCollateralBalanceChange(-1 ether); + + // Verify the lock request + assertTrue(boundlessMarket.requestIsLocked(request.id), "Request should be locked-in"); + + expectMarketBalanceUnchanged(); + + return (client, request); + } + + function testLockRequest() public returns (Client, ProofRequest memory) { + return _testLockRequest(false, "lockinRequest: base case"); + } + + function testLockRequestWithSignature() public returns (Client, ProofRequest memory) { + return _testLockRequest(true, "lockinRequest: with prover signature"); + } + + function _testLockRequestAlreadyLocked(bool withSig) private { + (Client client, ProofRequest memory request) = _testLockRequest(withSig); + bytes memory clientSignature = client.sign(request); + bytes memory proverSignature = testProver.signLockRequest(LockRequest({request: request})); + + // Attempt to lock the request again + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.RequestIsLocked.selector, request.id)); + if (withSig) { + boundlessMarket.lockRequestWithSignature(request, clientSignature, proverSignature); + } else { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } + + expectMarketBalanceUnchanged(); + } + + function testLockRequestAlreadyLocked() public { + return _testLockRequestAlreadyLocked(true); + } + + function testLockRequestWithSignatureAlreadyLocked() public { + return _testLockRequestAlreadyLocked(false); + } + + function _testLockRequestBadClientSignature(bool withSig) private { + Client clientA = getClient(1); + Client clientB = getClient(2); + ProofRequest memory request1 = clientA.request(1); + ProofRequest memory request2 = clientA.request(2); + bytes memory proverSignature = testProver.signLockRequest(LockRequest({request: request1})); + + // case: request signed by a different client + bytes memory badClientSignature = clientB.sign(request1); + vm.expectRevert(IBoundlessMarket.InvalidSignature.selector); + if (withSig) { + boundlessMarket.lockRequestWithSignature(request1, badClientSignature, proverSignature); + } else { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request1, badClientSignature); + } + + // case: client signed a different request + badClientSignature = clientA.sign(request2); + vm.expectRevert(IBoundlessMarket.InvalidSignature.selector); + if (withSig) { + boundlessMarket.lockRequestWithSignature(request1, badClientSignature, proverSignature); + } else { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request1, badClientSignature); + } + + clientA.expectBalanceChange(0 ether); + clientB.expectBalanceChange(0 ether); + testProver.expectBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + } + + function testLockRequestBadClientSignature() public { + return _testLockRequestBadClientSignature(true); + } + + function testLockRequestWithSignatureBadClientSignature() public { + return _testLockRequestBadClientSignature(false); + } + + /// @dev Address recovered from a deliberately-mismatched prover signature in the + /// two tests below. It is a deterministic function of the EIP-712 domain + /// separator, which depends on the proxy address and thus on the setUp + /// deployment sequence. The via-fallback suite overrides these because its + /// extra CREATE (the new market impl) shifts the proxy address. + function _expectedIncorrectRequestSigner() internal pure virtual returns (address) { + return address(0x013a129A6254FDb452a94b92385645b7959A7c5A); + } + + function _expectedIncorrectDomainSigner() internal pure virtual returns (address) { + return address(0x2949a308c21BD8bC839EFeCD4465cBebdE3F7388); + } + + function testLockRequestWithSignatureProverSignatureIncorrectRequest() public { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + bytes memory clientSignature = client.sign(request); + // Prover signs the incorrect request. + bytes memory badProverSignature = testProver.signLockRequest(LockRequest({request: client.request(2)})); + + // NOTE: Error is "InsufficientBalance" because we will recover _some_ address. + // It should be random and never correspond to a real account. + // TODO: The expected address (see _expectedIncorrectRequestSigner) will need to + // change anytime we change the ProofRequest struct or the way it is hashed for + // signatures. Find a good way to avoid this. + vm.expectRevert( + abi.encodeWithSelector(IBoundlessMarket.InsufficientBalance.selector, _expectedIncorrectRequestSigner()) + ); + boundlessMarket.lockRequestWithSignature(request, clientSignature, badProverSignature); + + client.expectBalanceChange(0 ether); + testProver.expectBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + } + + function testLockRequestWithSignatureProverSignatureIncorrectDomain() public { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + bytes memory clientSignature = client.sign(request); + // Prover signs ProofRequest struct rather than LockRequest struct. + // NOTE: This was how the contract worked in a previous version. This is included as a regression test. + bytes memory badProverSignature = testProver.sign(request); + + // NOTE: Error is "InsufficientBalance" because we will recover _some_ address. + // It should be random and never correspond to a real account. + // TODO: The expected address (see _expectedIncorrectDomainSigner) will need to + // change anytime we change the ProofRequest struct or the way it is hashed for + // signatures. Find a good way to avoid this. + vm.expectRevert( + abi.encodeWithSelector(IBoundlessMarket.InsufficientBalance.selector, _expectedIncorrectDomainSigner()) + ); + boundlessMarket.lockRequestWithSignature(request, clientSignature, badProverSignature); + + client.expectBalanceChange(0 ether); + testProver.expectBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + } + + function _testLockRequestNotEnoughFunds(bool withSig) private { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + bytes memory clientSignature = client.sign(request); + bytes memory proverSignature = testProver.signLockRequest(LockRequest({request: request})); + + address clientAddress = client.addr(); + vm.prank(clientAddress); + boundlessMarket.withdraw(DEFAULT_BALANCE); + + // case: client does not have enough funds to cover for the lock request + // should revert with "InsufficientBalance(address requester)" + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.InsufficientBalance.selector, client.addr())); + if (withSig) { + boundlessMarket.lockRequestWithSignature(request, clientSignature, proverSignature); + } else { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } + + vm.prank(clientAddress); + boundlessMarket.deposit{value: DEFAULT_BALANCE}(); + + vm.prank(testProverAddress); + boundlessMarket.withdrawCollateral(DEFAULT_BALANCE); + // case: prover does not have enough funds to cover for the lock request stake + // should revert with "InsufficientBalance(address requester)" + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.InsufficientBalance.selector, testProverAddress)); + if (withSig) { + boundlessMarket.lockRequestWithSignature(request, clientSignature, proverSignature); + } else { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } + } + + function testLockRequestNotEnoughFunds() public { + return _testLockRequestNotEnoughFunds(true); + } + + function testLockRequestWithSignatureNotEnoughFunds() public { + return _testLockRequestNotEnoughFunds(false); + } + + function _testLockRequestExpired(bool withSig) private { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + bytes memory clientSignature = client.sign(request); + bytes memory proverSignature = testProver.signLockRequest(LockRequest({request: request})); + + vm.warp(request.offer.deadline() + 1); + + // Attempt to lock the request after it has expired + // should revert with "RequestIsExpired({requestId: request.id, deadline: deadline})" + vm.expectRevert( + abi.encodeWithSelector( + IBoundlessMarket.RequestLockIsExpired.selector, request.id, request.offer.lockDeadline() + ) + ); + if (withSig) { + boundlessMarket.lockRequestWithSignature(request, clientSignature, proverSignature); + } else { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } + + expectMarketBalanceUnchanged(); + } + + function testLockRequestExpired() public { + return _testLockRequestExpired(true); + } + + function testLockRequestWithSignatureExpired() public { + return _testLockRequestExpired(false); + } + + function _testLockRequestLockExpired(bool withSig) private { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + bytes memory clientSignature = client.sign(request); + bytes memory proverSignature = testProver.signLockRequest(LockRequest({request: request})); + + vm.warp(request.offer.lockDeadline() + 1); + + vm.expectRevert( + abi.encodeWithSelector( + IBoundlessMarket.RequestLockIsExpired.selector, request.id, request.offer.lockDeadline() + ) + ); + if (withSig) { + boundlessMarket.lockRequestWithSignature(request, clientSignature, proverSignature); + } else { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } + + expectMarketBalanceUnchanged(); + } + + function testLockRequestLockExpired() public { + return _testLockRequestLockExpired(true); + } + + function testLockRequestWithSignatureLockExpired() public { + return _testLockRequestLockExpired(false); + } + + function _testLockRequestInvalidRequest1(bool withSig) private { + Offer memory offer = Offer({ + minPrice: 2 ether, + maxPrice: 1 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(0), + lockTimeout: uint32(1), + timeout: uint32(1), + lockCollateral: 10 ether + }); + + Client client = getClient(1); + ProofRequest memory request = client.request(1, offer); + bytes memory clientSignature = client.sign(request); + bytes memory proverSignature = testProver.signLockRequest(LockRequest({request: request})); + + // Attempt to lock a request with maxPrice smaller than minPrice + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.InvalidRequest.selector)); + if (withSig) { + boundlessMarket.lockRequestWithSignature(request, clientSignature, proverSignature); + } else { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } + + expectMarketBalanceUnchanged(); + } + + function testLockRequestInvalidRequest1() public { + return _testLockRequestInvalidRequest1(true); + } + + function testLockRequestWithSignatureInvalidRequest1() public { + return _testLockRequestInvalidRequest1(false); + } + + function _testLockRequestInvalidRequest2(bool withSig) private { + Offer memory offer = Offer({ + minPrice: 1 ether, + maxPrice: 1 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(2), + lockTimeout: uint32(1), + timeout: uint32(1), + lockCollateral: 10 ether + }); + + Client client = getClient(1); + ProofRequest memory request = client.request(1, offer); + bytes memory clientSignature = client.sign(request); + bytes memory proverSignature = testProver.signLockRequest(LockRequest({request: request})); + + // Attempt to lock a request with rampUpPeriod greater than timeout + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.InvalidRequest.selector)); + if (withSig) { + boundlessMarket.lockRequestWithSignature(request, clientSignature, proverSignature); + } else { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } + + expectMarketBalanceUnchanged(); + } + + function testLockRequestInvalidRequest2() public { + return _testLockRequestInvalidRequest2(true); + } + + function testLockRequestWithSignatureInvalidRequest2() public { + return _testLockRequestInvalidRequest2(false); + } + + enum LockRequestMethod { + LockRequest, + LockRequestWithSig, + None + } + + function _testFulfillSameBlock(uint32 requestIdx, LockRequestMethod lockinMethod) + private + returns (Client, ProofRequest memory) + { + return _testFulfillSameBlock(requestIdx, lockinMethod, ""); + } + + // Base for fulfillment tests with different methods for lock, including none. All paths should yield the same result. + function _testFulfillSameBlock(uint32 requestIdx, LockRequestMethod lockinMethod, string memory snapshot) + private + returns (Client, ProofRequest memory) + { + Client client = getClient(1); + ProofRequest memory request = client.request(requestIdx); + bytes memory clientSignature = client.sign(request); + + client.snapshotBalance(); + testProver.snapshotBalance(); + + if (lockinMethod == LockRequestMethod.LockRequest) { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } else if (lockinMethod == LockRequestMethod.LockRequestWithSig) { + boundlessMarket.lockRequestWithSignature( + request, clientSignature, testProver.signLockRequest(LockRequest({request: request})) + ); + } + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + if (lockinMethod == LockRequestMethod.None) { + // Annoying boilerplate for creating singleton lists. + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = client.sign(request); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fills[0].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + if (!_stringEquals(snapshot, "")) { + vm.snapshotGasLastCall(snapshot); + } + } else { + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fills[0].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + boundlessMarket.fulfill(fills, assessorReceipt); + if (!_stringEquals(snapshot, "")) { + vm.snapshotGasLastCall(snapshot); + } + } + + // Check that the proof was submitted + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + + return (client, request); + } + + // Base for fulfillment tests with deprecated assessor. + function _testFulfillDeprecatedAssessor(uint32 requestIdx) private { + Client client = getClient(1); + ProofRequest memory request = client.request(requestIdx); + bytes memory clientSignature = client.sign(request); + + client.snapshotBalance(); + testProver.snapshotBalance(); + + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createDeprecatedFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + if (block.timestamp <= boundlessMarket.DEPRECATED_ASSESSOR_EXPIRES_AT()) { + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fills[0].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + boundlessMarket.fulfill(fills, assessorReceipt); + + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + } else { + vm.expectRevert(VerificationFailed.selector); + boundlessMarket.fulfill(fills, assessorReceipt); + } + + expectMarketBalanceUnchanged(); + } + + // Base for fulfillmentAndWithdraw tests with different methods for lock, including none. All paths should yield the same result. + function _testFulfillAndWithdrawSameBlock(uint32 requestIdx, LockRequestMethod lockinMethod, string memory snapshot) + private + returns (Client, ProofRequest memory) + { + Client client = getClient(1); + ProofRequest memory request = client.request(requestIdx); + bytes memory clientSignature = client.sign(request); + + client.snapshotBalance(); + testProver.snapshotBalance(); + + if (lockinMethod == LockRequestMethod.LockRequest) { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } else if (lockinMethod == LockRequestMethod.LockRequestWithSig) { + boundlessMarket.lockRequestWithSignature( + request, clientSignature, testProver.signLockRequest(LockRequest({request: request})) + ); + } + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + uint256 initialBalance = boundlessMarket.balanceOf(testProverAddress) + testProverAddress.balance; + + if (lockinMethod == LockRequestMethod.None) { + // Annoying boilerplate for creating singleton lists. + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = client.sign(request); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fills[0].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + boundlessMarket.priceAndFulfillAndWithdraw(requests, clientSignatures, fills, assessorReceipt); + if (!_stringEquals(snapshot, "")) { + vm.snapshotGasLastCall(snapshot); + } + } else { + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fills[0].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + boundlessMarket.fulfillAndWithdraw(fills, assessorReceipt); + if (!_stringEquals(snapshot, "")) { + vm.snapshotGasLastCall(snapshot); + } + } + + // Check that the proof was submitted + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(-1 ether); + assert(boundlessMarket.balanceOf(testProverAddress) == 0); + assert(testProverAddress.balance == initialBalance + 1 ether); + + return (client, request); + } + + // Base for submitRoot and fulfillment tests with different methods for lock, including none. All paths should yield the same result. + function _testSubmitRootAndFulfillSameBlock( + uint32 requestIdx, + LockRequestMethod lockinMethod, + string memory snapshot + ) private returns (Client, ProofRequest memory) { + Client client = getClient(1); + ProofRequest memory request = client.request(requestIdx); + bytes memory clientSignature = client.sign(request); + + client.snapshotBalance(); + testProver.snapshotBalance(); + + if (lockinMethod == LockRequestMethod.LockRequest) { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } else if (lockinMethod == LockRequestMethod.LockRequestWithSig) { + boundlessMarket.lockRequestWithSignature( + request, clientSignature, testProver.signLockRequest(LockRequest({request: request})) + ); + } + + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes[] memory journals = new bytes[](1); + journals[0] = APP_JOURNAL; + + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) = + createFills(requests, journals, testProverAddress); + + bytes memory seal = + verifier.mockProve( + SET_BUILDER_IMAGE_ID, sha256(abi.encodePacked(SET_BUILDER_IMAGE_ID, uint256(1 << 255), root)) + ) + .seal; + + if (lockinMethod == LockRequestMethod.None) { + // Annoying boilerplate for creating singleton lists. + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = client.sign(request); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fills[0].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fills[0]); + boundlessMarket.submitRootAndPriceAndFulfill( + address(setVerifier), root, seal, requests, clientSignatures, fills, assessorReceipt + ); + if (!_stringEquals(snapshot, "")) { + vm.snapshotGasLastCall(snapshot); + } + } else { + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fills[0].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fills[0]); + boundlessMarket.submitRootAndPriceAndFulfill( + address(setVerifier), root, seal, new ProofRequest[](0), new bytes[](0), fills, assessorReceipt + ); + if (!_stringEquals(snapshot, "")) { + vm.snapshotGasLastCall(snapshot); + } + } + + // Check that the proof was submitted + expectRequestFulfilled(fills[0].id); + + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + + return (client, request); + } + + // Base for submitRootAndFulfillAndWithdraw tests with different methods for lock, including none. All paths should yield the same result. + function _testSubmitRootAndFulfillAndWithdrawSameBlock( + uint32 requestIdx, + LockRequestMethod lockinMethod, + string memory snapshot + ) private returns (Client, ProofRequest memory) { + Client client = getClient(1); + ProofRequest memory request = client.request(requestIdx); + bytes memory clientSignature = client.sign(request); + + client.snapshotBalance(); + testProver.snapshotBalance(); + + if (lockinMethod == LockRequestMethod.LockRequest) { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + } else if (lockinMethod == LockRequestMethod.LockRequestWithSig) { + boundlessMarket.lockRequestWithSignature( + request, clientSignature, testProver.signLockRequest(LockRequest({request: request})) + ); + } + + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes[] memory journals = new bytes[](1); + journals[0] = APP_JOURNAL; + + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) = + createFills(requests, journals, testProverAddress); + + bytes memory seal = + verifier.mockProve( + SET_BUILDER_IMAGE_ID, sha256(abi.encodePacked(SET_BUILDER_IMAGE_ID, uint256(1 << 255), root)) + ) + .seal; + + uint256 initialBalance = boundlessMarket.balanceOf(testProverAddress) + testProverAddress.balance; + + if (lockinMethod == LockRequestMethod.None) { + // Annoying boilerplate for creating singleton lists. + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = client.sign(request); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fills[0].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fills[0]); + boundlessMarket.submitRootAndPriceAndFulfillAndWithdraw( + address(setVerifier), root, seal, requests, clientSignatures, fills, assessorReceipt + ); + if (!_stringEquals(snapshot, "")) { + vm.snapshotGasLastCall(snapshot); + } + } else { + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fills[0].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fills[0]); + boundlessMarket.submitRootAndPriceAndFulfillAndWithdraw( + address(setVerifier), root, seal, new ProofRequest[](0), new bytes[](0), fills, assessorReceipt + ); + if (!_stringEquals(snapshot, "")) { + vm.snapshotGasLastCall(snapshot); + } + } + + // Check that the proof was submitted + expectRequestFulfilled(fills[0].id); + + client.expectBalanceChange(-1 ether); + assert(boundlessMarket.balanceOf(testProverAddress) == 0); + assert(testProverAddress.balance == initialBalance + 1 ether); + + return (client, request); + } + + function testFulfillLockedRequest() public { + _testFulfillSameBlock(1, LockRequestMethod.LockRequest, "fulfill: a locked request"); + } + + function testFulfillAndWithdrawLockedRequest() public { + _testFulfillAndWithdrawSameBlock(1, LockRequestMethod.LockRequest, "fulfillAndWithdraw: a locked request"); + } + + function testFulfillLockedRequestWithSig() public { + _testFulfillSameBlock( + 1, LockRequestMethod.LockRequestWithSig, "fulfill: a locked request (locked via prover signature)" + ); + } + + function testFulfillDeprecatedAssessor() public { + _testFulfillDeprecatedAssessor(1); + // Warp past the deprecated assessor expiration time + vm.warp(block.timestamp + DEPRECATED_ASSESSOR_DURATION + 1 minutes); + _testFulfillDeprecatedAssessor(2); + } + + function testSubmitRootAndFulfillLockedRequest() public { + _testSubmitRootAndFulfillSameBlock(1, LockRequestMethod.LockRequest, "submitRootAndFulfill: a locked request"); + } + + function testSubmitRootAndFulfillAndWithdrawLockedRequest() public { + _testSubmitRootAndFulfillAndWithdrawSameBlock( + 1, LockRequestMethod.LockRequest, "submitRootAndFulfillAndWithdraw: a locked request" + ); + } + + function testSubmitRootAndFulfillLockedRequestWithSig() public { + _testSubmitRootAndFulfillSameBlock( + 1, + LockRequestMethod.LockRequestWithSig, + "submitRootAndFulfill: a locked request (locked via prover signature)" + ); + } + + // Check that a single client can create many requests, with the full range of indices, and + // complete the flow each time. + function testFulfillLockedRequestRangeOfRequestIdx() public { + for (uint32 idx = 0; idx < 512; idx++) { + _testFulfillSameBlock(idx, LockRequestMethod.LockRequest); + } + _testFulfillSameBlock(0xdeadbeef, LockRequestMethod.LockRequest); + _testFulfillSameBlock(0xffffffff, LockRequestMethod.LockRequest); + } + + function testFulfillLargeJournal() external { + // Generate a 10kB buffer full of non-zero bytes. + // 10kB = 320 bytes32 values (10240/32) + bytes32[] memory buffer32 = new bytes32[](320); + for (uint256 i = 0; i < buffer32.length; i++) { + buffer32[i] = bytes32(uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)); + } + bytes memory bigJournal = abi.encodePacked(buffer32); + + Client client = getClient(1); + ProofRequest memory request = client.request(1); + request.requirements.predicate = + Predicate({predicateType: PredicateType.DigestMatch, data: abi.encode(sha256(bigJournal))}); + bytes memory clientSignature = client.sign(request); + + client.snapshotBalance(); + testProver.snapshotBalance(); + + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, bigJournal, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + boundlessMarket.fulfill(fills, assessorReceipt); + vm.snapshotGasLastCall("fulfill: a locked request with 10kB journal"); + + // Check that the proof was submitted + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } + + // While a request is locked, another prover can fulfill it but will not receive a payment. + function testFulfillLockedRequestByOtherProverNotRequirePayment() + public + returns (Client, Client, ProofRequest memory) + { + Client client = getClient(1); + ProofRequest memory request = client.request(3); + + boundlessMarket.lockRequestWithSignature( + request, client.sign(request), testProver.signLockRequest(LockRequest({request: request})) + ); + + Client otherProver = getProver(2); + address otherProverAddress = otherProver.addr(); + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, otherProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsLocked.selector, request.id + )); + boundlessMarket.fulfill(fills, assessorReceipt); + vm.snapshotGasLastCall("fulfill: another prover fulfills without payment"); + + expectRequestFulfilled(fill.id); + + // Provers stake is still on the line. + testProver.expectCollateralBalanceChange(-int256(uint256(request.offer.lockCollateral))); + + // No payment should have been made, as the other prover filled while the request is still locked. + otherProver.expectBalanceChange(0); + otherProver.expectCollateralBalanceChange(0); + + expectMarketBalanceUnchanged(); + + return (client, otherProver, request); + } + + // If a request was fulfilled and payment was already sent, we don't allow it to be fulfilled again. + function testFulfillLockedRequestAlreadyFulfilledAndPaid() public { + _testFulfillAlreadyFulfilled(1, LockRequestMethod.LockRequest); + _testFulfillAlreadyFulfilled(2, LockRequestMethod.LockRequestWithSig); + } + + // This is the only case where fulfill can be called twice successfully. + // In some cases, a request can be fulfilled without payment being sent. This test starts with + // one of those cases and checks that the prover can submit fulfillment again to get payment. + function testFulfillLockedRequestAlreadyFulfilledByOtherProver() public { + (, Client otherProver, ProofRequest memory request) = testFulfillLockedRequestByOtherProverNotRequirePayment(); + testProver.snapshotBalance(); + testProver.snapshotCollateralBalance(); + otherProver.snapshotBalance(); + otherProver.snapshotCollateralBalance(); + + expectRequestFulfilled(request.id); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + boundlessMarket.fulfill(fills, assessorReceipt); + vm.snapshotGasLastCall( + "fulfill: fulfilled by the locked prover for payment (request already fulfilled by another prover)" + ); + + expectRequestFulfilled(request.id); + + // Prover should now have received back their stake plus payment for the request. + testProver.expectBalanceChange(1 ether); + testProver.expectCollateralBalanceChange(1 ether); + + // No payment should have been made to the other prover that filled while the request was locked. + otherProver.expectBalanceChange(0); + otherProver.expectCollateralBalanceChange(0); + + expectMarketBalanceUnchanged(); + } + + function testFulfillLockedRequestProverAddressNotMatchAssessorReceipt() public { + Client client = getClient(1); + + ProofRequest memory request = client.request(3); + + boundlessMarket.lockRequestWithSignature( + request, client.sign(request), testProver.signLockRequest(LockRequest({request: request})) + ); + // address(3) is just a standin for some other address. + address mockOtherProverAddr = address(uint160(3)); + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + assessorReceipt.prover = mockOtherProverAddr; + vm.expectRevert(VerificationFailed.selector); + boundlessMarket.fulfill(fills, assessorReceipt); + + // Prover should have their original balance less the stake amount. + testProver.expectCollateralBalanceChange(-int256(uint256(request.offer.lockCollateral))); + expectMarketBalanceUnchanged(); + } + + // Tests trying to fulfill a request that was locked and has now expired. + function testFulfillLockedRequestFullyExpired() public returns (Client, ProofRequest memory) { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + client.snapshotBalance(); + testProver.snapshotBalance(); + + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + // At this point the client should have only been charged the 1 ETH at lock time. + client.expectBalanceChange(-1 ether); + + // Advance the chain ahead to simulate the request timeout. + vm.warp(request.offer.deadline() + 1); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + // Try the priceAndFulfill path. + bytes[] memory paymentErrors = + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + assert( + keccak256(paymentErrors[0]) + == keccak256(abi.encodeWithSelector(IBoundlessMarket.RequestIsExpired.selector, request.id)) + ); + expectRequestNotFulfilled(fill.id); + + // Client is out 1 eth until slash is called. + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(0 ether); + testProver.expectCollateralBalanceChange(-1 ether); + expectMarketBalanceUnchanged(); + + // Try the fulfill path as well. Should be the same results. + paymentErrors = boundlessMarket.fulfill(fills, assessorReceipt); + assert( + keccak256(paymentErrors[0]) + == keccak256(abi.encodeWithSelector(IBoundlessMarket.RequestIsExpired.selector, request.id)) + ); + expectRequestNotFulfilled(fill.id); + + // Client is out 1 eth until slash is called. + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(0 ether); + testProver.expectCollateralBalanceChange(-1 ether); + expectMarketBalanceUnchanged(); + + return (client, request); + } + + function testFulfillLockedRequestMultipleRequestsSameIndex() public { + _testFulfillRepeatIndex(LockRequestMethod.LockRequest); + } + + function testFulfillLockedRequestMultipleRequestsSameIndexWithSig() public { + _testFulfillRepeatIndex(LockRequestMethod.LockRequestWithSig); + } + + // Scenario when a prover locks a request, fails to deliver it within the lock expiry, + // then another prover fulfills a request after the lock has expired, + // but before the request as a whole has expired. + function testFulfillWasLockedRequestByOtherProver() public returns (ProofRequest memory, Client, Client, Client) { + // Create a request with a lock timeout of 50 blocks, and overall timeout of 100. + Client client = getClient(1); + ProofRequest memory request = client.request( + 1, + Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(50), + lockTimeout: uint32(50), + timeout: uint32(100), + lockCollateral: 1 ether + }) + ); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + Client locker = getProver(1); + Client otherProver = getProver(2); + + client.snapshotBalance(); + locker.snapshotBalance(); + otherProver.snapshotBalance(); + + address lockerAddress = locker.addr(); + vm.prank(lockerAddress); + boundlessMarket.lockRequest(request, clientSignature); + // At this point the client should have only been charged the 1 ETH at lock time. + client.expectBalanceChange(-1 ether); + + // Advance the chain ahead to simulate the lock timeout. + vm.warp(request.offer.lockDeadline() + 1); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, otherProver.addr()); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, otherProver.addr(), fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, otherProver.addr(), fill); + + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + // Check that the proof was submitted + expectRequestFulfilled(fill.id); + + // Client's fee should be returned on fulfill. + client.expectBalanceChange(0 ether); + locker.expectBalanceChange(0 ether); + locker.expectCollateralBalanceChange(-1 ether); + otherProver.expectBalanceChange(0 ether); + otherProver.expectCollateralBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + + return (request, client, locker, otherProver); + } + + function testFulfillWasLockedClientWithdrawsBalance() public { + Client client = getClient(1); + ProofRequest memory request = client.request( + 1, + Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(50), + lockTimeout: uint32(50), + timeout: uint32(100), + lockCollateral: 1 ether + }) + ); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + address clientAddress = client.addr(); + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + + uint256 balance = boundlessMarket.balanceOf(clientAddress); + vm.prank(clientAddress); + boundlessMarket.withdraw(balance); + + client.snapshotBalance(); + + // Advance the chain ahead to simulate the lock timeout. + vm.warp(request.offer.lockDeadline() + 1); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + // Fulfill should complete successfully. + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + expectRequestFulfilled(fill.id); + + // Client should get back 1 eth upon fulfill. + client.expectBalanceChange(1 ether); + testProver.expectBalanceChange(0 ether); + testProver.expectCollateralBalanceChange(-1 ether); + } + + // Scenario when a prover locks a request, fails to deliver it within the lock expiry, + // but does deliver it before the request expires. Here they should lose their stake, + // but receive payment for the request. + function testFulfillWasLockedRequestByOriginalLocker() public returns (ProofRequest memory, Client) { + // Create a request with a lock timeout of 50 blocks, and overall timeout of 100. + Client client = getClient(1); + ProofRequest memory request = client.request( + 1, + Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(50), + lockTimeout: uint32(50), + timeout: uint32(100), + lockCollateral: 1 ether + }) + ); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + Client locker = getProver(1); + + client.snapshotBalance(); + locker.snapshotBalance(); + + address lockerAddress = locker.addr(); + vm.prank(lockerAddress); + boundlessMarket.lockRequest(request, clientSignature); + + // Advance the chain ahead to simulate the lock timeout. + vm.warp(request.offer.lockDeadline() + 1); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, locker.addr()); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, lockerAddress, fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, lockerAddress, fill); + + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + // Check that the proof was submitted + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(0 ether); + locker.expectBalanceChange(0 ether); + locker.expectCollateralBalanceChange(-1 ether); + expectMarketBalanceUnchanged(); + return (request, locker); + } + + // One request is locked, fully expires. + // A second request with the same id is then fulfilled. + // Slash should award stake to the fulfiller of the second request. + function testFulfillWasLockedRequestRepeatIndexStakeRollover() public { + Client client = getClient(1); + + Offer memory offerA = Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(10), + lockTimeout: uint32(100), + timeout: uint32(100), + lockCollateral: 1 ether + }); + Offer memory offerB = Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp) + uint64(offerA.timeout) + 1, + rampUpPeriod: uint32(10), + lockTimeout: uint32(100), + timeout: 100, + lockCollateral: 1 ether + }); + + ProofRequest memory requestA = client.request(1, offerA); + ProofRequest memory requestB = client.request(1, offerB); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = requestB; + bytes memory clientSignatureA = client.sign(requestA); + bytes memory clientSignatureB = client.sign(requestB); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignatureB; + Client locker = getProver(1); + Client fulfiller = getProver(2); + + client.snapshotBalance(); + locker.snapshotBalance(); + fulfiller.snapshotBalance(); + + // Lock-in request A. + address lockerAddress = locker.addr(); + vm.prank(lockerAddress); + boundlessMarket.lockRequest(requestA, clientSignatureA); + + vm.warp(uint64(block.timestamp) + uint64(offerA.timeout) + 1); + // Attempt to fill request B. + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(requestB, APP_JOURNAL, fulfiller.addr()); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + // Check that the request ID is marked as fulfilled. + expectRequestFulfilled(fill.id); + + boundlessMarket.slash(fill.id); + + client.expectBalanceChange(-1 ether); + locker.expectBalanceChange(0 ether); + locker.expectCollateralBalanceChange(-1 ether); + fulfiller.expectBalanceChange(1 ether); + fulfiller.expectCollateralBalanceChange(uint256(expectedSlashTransferAmount(offerA.lockCollateral)).toInt256()); + expectMarketBalanceUnchanged(); + } + + // One request is locked, the lock expires, but the request is not yet expired. + // A second request with the same id is then fulfilled. + // Slash should award stake to the fulfiller of the second request. + function testFulfillWasLockedRequestRepeatIndexStakeRolloverFirstRequestNotExpired() public { + Client client = getClient(1); + + Offer memory offerA = Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(10), + lockTimeout: uint32(50), + timeout: uint32(100), + lockCollateral: 1 ether + }); + Offer memory offerB = Offer({ + minPrice: 2 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(0), + lockTimeout: offerA.timeout + 101, + timeout: offerA.timeout + 101, + lockCollateral: 1 ether + }); + + ProofRequest memory requestA = client.request(1, offerA); + ProofRequest memory requestB = client.request(1, offerB); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = requestB; + bytes memory clientSignatureA = client.sign(requestA); + bytes memory clientSignatureB = client.sign(requestB); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignatureB; + Client locker = getProver(1); + Client fulfiller = getProver(2); + + client.snapshotBalance(); + locker.snapshotBalance(); + fulfiller.snapshotBalance(); + + // Lock-in request A. + address lockerAddress = locker.addr(); + vm.prank(lockerAddress); + boundlessMarket.lockRequest(requestA, clientSignatureA); + + vm.warp(offerA.lockDeadline() + 1); + // Attempt to fill request B. + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(requestB, APP_JOURNAL, fulfiller.addr()); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + // Check that the request ID is marked as fulfilled. + expectRequestFulfilled(fill.id); + + // Slash should revert as the original locked request has not yet fully expired. + vm.expectRevert( + abi.encodeWithSelector( + IBoundlessMarket.RequestIsNotExpired.selector, fill.id, uint64(block.timestamp) + uint64(offerA.timeout) + ) + ); + boundlessMarket.slash(fill.id); + + // Advance to where the original locked request has fully expired. + vm.warp(uint64(block.timestamp) + uint64(offerA.timeout) + 1); + + vm.prank(lockerAddress); + boundlessMarket.slash(fill.id); + + client.expectBalanceChange(-2 ether); + locker.expectBalanceChange(0 ether); + locker.expectCollateralBalanceChange(-1 ether); + fulfiller.expectBalanceChange(2 ether); + fulfiller.expectCollateralBalanceChange(uint256(expectedSlashTransferAmount(offerA.lockCollateral)).toInt256()); + expectMarketBalanceUnchanged(); + } + + // One request is locked and the client is charged 2 ether. The request expires unfulfilled. + // A second request with the same id is then fulfilled for a cost of just 1 ether. + // The client should be refunded the difference. + function testFulfillWasLockedRequestRepeatIndexSecondRequestCheaper() public { + Client client = getClient(1); + + // Create two distinct requests with the same ID. It should be the case that only one can be + // filled, and if one is locked, the other cannot be filled. + Offer memory offerA = Offer({ + minPrice: 2 ether, + maxPrice: 3 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(10), + lockTimeout: uint32(50), + timeout: uint32(100), + lockCollateral: 1 ether + }); + Offer memory offerB = Offer({ + minPrice: 1 ether, + maxPrice: 1 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(0), + lockTimeout: uint32(100), + timeout: uint32(block.timestamp) + offerA.timeout + 101, + lockCollateral: 1 ether + }); + + ProofRequest memory requestA = client.request(1, offerA); + ProofRequest memory requestB = client.request(1, offerB); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = requestB; + bytes memory clientSignatureA = client.sign(requestA); + bytes memory clientSignatureB = client.sign(requestB); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignatureB; + Client locker = getProver(1); + Client fulfiller = getProver(2); + + client.snapshotBalance(); + locker.snapshotBalance(); + fulfiller.snapshotBalance(); + + // Lock-in request A. + address lockerAddress = locker.addr(); + vm.prank(lockerAddress); + boundlessMarket.lockRequest(requestA, clientSignatureA); + + client.expectBalanceChange(-2 ether); + + vm.warp(offerA.lockDeadline() + 1); + + // Attempt to fill request B, which costs just 1 ether at the time of fulfillment. + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(requestB, APP_JOURNAL, fulfiller.addr()); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + // Client should be refunded 1 ether, meaning their net balance change is -1 + client.expectBalanceChange(-1 ether); + + // Check that the request ID is marked as fulfilled. + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(-1 ether); + locker.expectBalanceChange(0 ether); + locker.expectCollateralBalanceChange(-1 ether); + fulfiller.expectBalanceChange(1 ether); + fulfiller.expectCollateralBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + } + + // One request is locked, expires, and is slashed. + // A second request with the same id is then fulfilled. + function testFulfillWasLockedRequestRepeatIndexStakeRolloverSlashedBeforeFulfill() public { + Client client = getClient(1); + + // Create two distinct requests with the same ID. It should be the case that only one can be + // filled, and if one is locked, the other cannot be filled. + Offer memory offerA = Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(10), + lockTimeout: uint32(100), + timeout: uint32(100), + lockCollateral: 1 ether + }); + Offer memory offerB = Offer({ + minPrice: 3 ether, + maxPrice: 3 ether, + rampUpStart: uint64(block.timestamp) + uint64(offerA.timeout) + 1, + rampUpPeriod: uint32(10), + lockTimeout: uint32(100), + timeout: 100, + lockCollateral: 1 ether + }); + + ProofRequest memory requestA = client.request(1, offerA); + ProofRequest memory requestB = client.request(1, offerB); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = requestB; + bytes memory clientSignatureA = client.sign(requestA); + bytes memory clientSignatureB = client.sign(requestB); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignatureB; + Client locker = getProver(1); + Client fulfiller = getProver(2); + + client.snapshotBalance(); + locker.snapshotBalance(); + fulfiller.snapshotBalance(); + + // Lock-in request A. + address lockerAddress = locker.addr(); + vm.prank(lockerAddress); + boundlessMarket.lockRequest(requestA, clientSignatureA); + + vm.warp(uint64(block.timestamp) + uint64(offerA.timeout) + 1); + + // Slash the request first. + vm.prank(lockerAddress); + boundlessMarket.slash(requestA.id); + + // Attempt to fill request B. + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(requestB, APP_JOURNAL, fulfiller.addr()); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + address fulfillerAddress = fulfiller.addr(); + vm.prank(fulfillerAddress); + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + // Check that the request ID is marked as fulfilled. + expectRequestFulfilledAndSlashed(fill.id); + + client.expectBalanceChange(-3 ether); + locker.expectBalanceChange(0 ether); + locker.expectCollateralBalanceChange(-1 ether); + fulfiller.expectBalanceChange(3 ether); + fulfiller.expectCollateralBalanceChange(0 ether); + } + + // Scenario when a prover locks a request, fails to deliver it within the lock expiry, + // but does deliver it before the request expires. Here they should lose most of their stake + // (not all), and receive no payment from the client. + function testFulfillWasLockedRequestDoubleFulfill() public { + // Create a request with a lock timeout of 50 blocks, and overall timeout of 100. + Client client = getClient(1); + ProofRequest memory request = client.request( + 1, + Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(50), + lockTimeout: uint32(50), + timeout: uint32(100), + lockCollateral: 1 ether + }) + ); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + Client locker = getProver(1); + address lockerAddress = locker.addr(); + + client.snapshotBalance(); + locker.snapshotBalance(); + + vm.prank(lockerAddress); + boundlessMarket.lockRequest(request, clientSignature); + + // Advance the chain ahead to simulate the lock timeout. + vm.warp(request.offer.lockDeadline() + 1); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, lockerAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsFulfilled.selector, request.id + )); + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + vm.snapshotGasLastCall("priceAndFulfill: fulfill already fulfilled was locked request"); + + // Check that the proof was submitted + expectRequestFulfilled(fill.id); + + // Check balances after the fulfillment but before slash. + client.expectBalanceChange(0 ether); + locker.expectBalanceChange(0 ether); + locker.expectCollateralBalanceChange(-1 ether); + + vm.warp(request.offer.deadline() + 1); + boundlessMarket.slash(request.id); + + // Check balances after the slash. + client.expectBalanceChange(0 ether); + locker.expectBalanceChange(0 ether); + locker.expectCollateralBalanceChange(-int256(uint256(expectedSlashBurnAmount(request.offer.lockCollateral)))); + } + + // Scenario when a prover locks a request, fails to deliver it within the lock expiry, + // another prover fulfills the request, and then the locker tries to fulfill the request + // before the request as a whole has expired. A proof should still be delivered and no revert + // should occur, since we support multiple proofs being delivered for a single request. No + // balance changes should occur. + function testFulfillWasLockedRequestLockerFulfillAfterAnotherProverFulfill() public { + (ProofRequest memory request, Client client, Client locker,) = testFulfillWasLockedRequestByOtherProver(); + + locker.snapshotBalance(); + locker.snapshotCollateralBalance(); + + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + // The locker should have no balance change. + // Now the locker tries to fulfill the request. + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, locker.addr()); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + // But its already been fulfilled by the other prover. + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsFulfilled.selector, request.id + )); + + // The proof should still be delivered. + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, locker.addr(), fill); + + // The fulfillment should not revert, as we support multiple proofs being delivered for a single request. + bytes[] memory paymentErrors = + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + assert( + keccak256(paymentErrors[0]) + == keccak256(abi.encodeWithSelector(IBoundlessMarket.RequestIsFulfilled.selector, request.id)) + ); + + // The locker should have no balance change. + locker.expectBalanceChange(0 ether); + locker.expectCollateralBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + } + + // Scenario when a prover locks a request, fails to deliver it within the lock expiry, + // another prover fulfills the request, and then the locker tries to fulfill the request + // _after_ the request has fully expired. + // + // In this case the request has fully expired, so the proof should NOT be delivered, + // however we should not revert (as this allows partial fulfillment of other requests in the batch). + function testFulfillWasLockedRequestLockerFulfillAfterAnotherProverFulfillAndRequestExpired() public { + (ProofRequest memory request, Client client, Client locker,) = testFulfillWasLockedRequestByOtherProver(); + + locker.snapshotBalance(); + locker.snapshotCollateralBalance(); + + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + // Advance the chain ahead to simulate the request expiration. + vm.warp(request.offer.deadline() + 1); + + // The locker should have no balance change. + // Now the locker tries to fulfill the request. + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, locker.addr()); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + // In this case the request has fully expired, so the proof should NOT be delivered, + // however we should not revert (as this allows partial fulfillment of other requests in the batch) + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsExpired.selector, request.id + )); + + // The fulfillment should not revert, as we support multiple proofs being delivered for a single request. + bytes[] memory paymentErrors = + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + assert( + keccak256(paymentErrors[0]) + == keccak256(abi.encodeWithSelector(IBoundlessMarket.RequestIsExpired.selector, request.id)) + ); + + // The locker should have no balance change. + locker.expectBalanceChange(0 ether); + locker.expectCollateralBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + } + + // A request is locked with a valid smart contract signature (signature is checked onchain at lock time) + // and then a prover tries to fulfill it specifying an invalid smart contract signature. The signature could + // be invalid for a number of reasons, including the smart contract wallet rotating their signers so the old signature + // is no longer valid. + // Since there is possibility of funds being pulled in the multiple request same id case, we ensure we check + // the SC signature again. + function testFulfillWasLockedRequestByInvalidSmartContractSignature() public { + SmartContractClient client = getSmartContractClient(1); + // Request ID indicates smart contract signature, but the signature is invalid. + ProofRequest memory request = client.request( + 1, + Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(50), + lockTimeout: uint32(50), + timeout: uint32(100), + lockCollateral: 1 ether + }) + ); + bytes memory validClientSignature = client.sign(request); + bytes memory invalidClientSignature = bytes("invalid"); + + boundlessMarket.lockRequestWithSignature( + request, validClientSignature, testProver.signLockRequest(LockRequest({request: request})) + ); + vm.warp(request.offer.lockDeadline() + 1); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + // Fulfill should succeed even though the lock has expired when the request matches what was locked. + boundlessMarket.fulfill(fills, assessorReceipt); + + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = invalidClientSignature; + // Fulfill should revert during the signature check during pricing, since the signature is invalid. + // NOTE: This should revert, even though we know the request was signed previously because + // of signature validation during the lock operation, because the signature in this call is + // invalid. As a principle, all data in a message must be validated, even if the data given + // is superfluous. + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.InvalidSignature.selector)); + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + clientSignatures[0] = validClientSignature; + // Fulfill should succeed if the signature is valid. + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(0 ether); + testProver.expectBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + } + + function testFulfillNeverLocked() public { + _testFulfillSameBlock(1, LockRequestMethod.None, "priceAndFulfill: a single request that was not locked"); + } + + /// Fulfill without locking should still work even if the prover does not have stake. + function testFulfillNeverLockedProverNoStake() public { + vm.prank(testProverAddress); + boundlessMarket.withdrawCollateral(DEFAULT_BALANCE); + + _testFulfillSameBlock( + 1, + LockRequestMethod.None, + "priceAndFulfill: a single request that was not locked fulfilled by prover not in allow-list" + ); + } + + function testSubmitRootAndFulfillNeverLocked() public { + _testSubmitRootAndFulfillSameBlock( + 1, LockRequestMethod.None, "submitRootAndPriceAndFulfill: a single request that was not locked" + ); + } + + /// SubmitRootAndFulfill without locking should still work even if the prover does not have stake. + function testSubmitRootAndFulfillNeverLockedProverNoStake() public { + vm.prank(testProverAddress); + boundlessMarket.withdrawCollateral(DEFAULT_BALANCE); + + _testSubmitRootAndFulfillSameBlock( + 1, + LockRequestMethod.None, + "submitRootAndPriceAndFulfill: a single request that was not locked fulfilled by prover not in allow-list" + ); + } + + function testFulfillNeverLockedNotPriced() public { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + // Attempt to fulfill a request without locking or pricing it. + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.RequestIsNotLockedOrPriced.selector, request.id)); + boundlessMarket.fulfill(fills, assessorReceipt); + + expectMarketBalanceUnchanged(); + } + + // Should revert as you can not fulfill a request twice, except for in the case covered by: + // `testFulfillLockedRequestAlreadyFulfilledByOtherProver` + function testFulfillNeverLockedAlreadyFulfilledAndPaid() public { + _testFulfillAlreadyFulfilled(3, LockRequestMethod.None); + } + + function testFulfillNeverLockedFullyExpired() public returns (Client, ProofRequest memory) { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.warp(request.offer.deadline() + 1); + + bytes[] memory paymentErrors = + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + assert( + keccak256(paymentErrors[0]) + == keccak256(abi.encodeWithSelector(IBoundlessMarket.RequestIsExpired.selector, request.id)) + ); + expectRequestNotFulfilled(fill.id); + + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.RequestIsNotLockedOrPriced.selector, request.id)); + boundlessMarket.fulfill(fills, assessorReceipt); + + expectRequestNotFulfilled(fill.id); + client.expectBalanceChange(0 ether); + testProver.expectBalanceChange(0 ether); + testProver.expectCollateralBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + + return (client, request); + } + + function testFulfillNeverLockedClientWithdrawsBalance() public { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + address clientAddress = client.addr(); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + uint256 balance = boundlessMarket.balanceOf(clientAddress); + vm.prank(clientAddress); + boundlessMarket.withdraw(balance); + + // expect emit of payment requirement failed + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.InsufficientBalance.selector, clientAddress + )); + vm.prank(clientAddress); + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + expectRequestFulfilled(fill.id); + } + + function testFulfillNeverLockedRequestMultipleRequestsSameIndex() public { + _testFulfillRepeatIndex(LockRequestMethod.None); + } + + // Fulfill a batch of locked requests + function testFulfillLockedRequests() public { + // Provide a batch definition as an array of clients and how many requests each submits. + uint256[5] memory batch = [uint256(1), 2, 1, 3, 1]; + uint256 batchSize = 0; + for (uint256 i = 0; i < batch.length; i++) { + batchSize += batch[i]; + } + ProofRequest[] memory requests = new ProofRequest[](batchSize); + bytes[] memory journals = new bytes[](batchSize); + uint256 expectedRevenue = 0; + uint256 idx = 0; + for (uint256 i = 0; i < batch.length; i++) { + Client client = getClient(i); + + for (uint256 j = 0; j < batch[i]; j++) { + ProofRequest memory request = client.request(uint32(j)); + + // TODO: This is a fragile part of this test. It should be improved. + uint256 desiredPrice = uint256(1.5 ether); + vm.warp(request.offer.timeAtPrice(desiredPrice)); + expectedRevenue += desiredPrice; + + boundlessMarket.lockRequestWithSignature( + request, client.sign(request), testProver.signLockRequest(LockRequest({request: request})) + ); + + requests[idx] = request; + journals[idx] = APP_JOURNAL; + idx++; + } + } + + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) = + createFillsAndSubmitRoot(requests, journals, testProverAddress); + + for (uint256 i = 0; i < fills.length; i++) { + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(fills[i].id, testProverAddress, fills[i].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(fills[i].id, testProverAddress, fills[i]); + } + boundlessMarket.fulfill(fills, assessorReceipt); + vm.snapshotGasLastCall(string.concat("fulfill: a batch of ", vm.toString(batchSize))); + + for (uint256 i = 0; i < fills.length; i++) { + // Check that the proof was submitted + expectRequestFulfilled(fills[i].id); + } + + testProver.expectBalanceChange(int256(uint256(expectedRevenue))); + expectMarketBalanceUnchanged(); + } + + // Fulfill a batch of locked ClaimDigestMatch requests with no journal + function testFulfillLockedRequestsNoJournal() public { + // Provide a batch definition as an array of clients and how many requests each submits. + uint256[5] memory batch = [uint256(1), 2, 1, 3, 1]; + uint256 batchSize = 0; + for (uint256 i = 0; i < batch.length; i++) { + batchSize += batch[i]; + } + ProofRequest[] memory requests = new ProofRequest[](batchSize); + bytes[] memory journals = new bytes[](batchSize); + uint256 expectedRevenue = 0; + uint256 idx = 0; + + for (uint256 i = 0; i < batch.length; i++) { + Client client = getClient(i); + + for (uint256 j = 0; j < batch[i]; j++) { + ProofRequest memory request = client.request(uint32(j)); + bytes32 imageId = bytesToBytes32(request.requirements.predicate.data); + + request.requirements.predicate = Predicate({ + predicateType: PredicateType.ClaimDigestMatch, + data: abi.encode(ReceiptClaimLib.ok(imageId, sha256(APP_JOURNAL)).digest()) + }); + + // TODO: This is a fragile part of this test. It should be improved. + uint256 desiredPrice = uint256(1.5 ether); + vm.warp(request.offer.timeAtPrice(desiredPrice)); + expectedRevenue += desiredPrice; + + boundlessMarket.lockRequestWithSignature( + request, client.sign(request), testProver.signLockRequest(LockRequest({request: request})) + ); + + requests[idx] = request; + journals[idx] = APP_JOURNAL; + idx++; + } + } + + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) = + createFillsAndSubmitRoot(requests, journals, testProverAddress, FulfillmentDataType.None); + + for (uint256 i = 0; i < fills.length; i++) { + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(fills[i].id, testProverAddress, fills[i].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(fills[i].id, testProverAddress, fills[i]); + } + boundlessMarket.fulfill(fills, assessorReceipt); + vm.snapshotGasLastCall(string.concat("fulfill (no journal): a batch of ", vm.toString(batchSize))); + for (uint256 i = 0; i < fills.length; i++) { + // Check that the proof was submitted + expectRequestFulfilled(fills[i].id); + } + + testProver.expectBalanceChange(int256(uint256(expectedRevenue))); + expectMarketBalanceUnchanged(); + } + + // Testing that reordering request IDs in a batch will cause the fulfill to revert. + function testFulfillShuffleIds() public { + uint256[5] memory batch = [uint256(1), 2, 1, 3, 1]; + uint256 batchSize = 0; + for (uint256 i = 0; i < batch.length; i++) { + batchSize += batch[i]; + } + ProofRequest[] memory requests = new ProofRequest[](batchSize); + bytes[] memory journals = new bytes[](batchSize); + bytes[] memory signatures = new bytes[](batchSize); + uint256 idx = 0; + for (uint256 i = 0; i < batch.length; i++) { + Client client = getClient(i); + + for (uint256 j = 0; j < batch[i]; j++) { + ProofRequest memory request = client.request(uint32(j)); + + requests[idx] = request; + journals[idx] = APP_JOURNAL; + signatures[idx] = client.sign(request); + idx++; + } + } + + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) = + createFillsAndSubmitRoot(requests, journals, testProverAddress); + + // Swap first two IDs + RequestId id0 = fills[0].id; + fills[0].id = fills[1].id; + fills[1].id = id0; + + vm.warp(requests[0].offer.timeAtPrice(uint256(1.5 ether))); + vm.expectRevert(VerificationFailed.selector); + boundlessMarket.priceAndFulfill(requests, signatures, fills, assessorReceipt); + + expectMarketBalanceUnchanged(); + } + + // Testing that reordering fulfillments in a batch will cause the fulfill to revert. + function testFulfillShuffleFills() public { + uint256 batchSize = 2; + ProofRequest[] memory requests = new ProofRequest[](batchSize); + bytes[] memory journals = new bytes[](batchSize); + + // First request + Client client = getClient(0); + ProofRequest memory request = client.request(uint32(0)); + boundlessMarket.lockRequestWithSignature( + request, client.sign(request), testProver.signLockRequest(LockRequest({request: request})) + ); + requests[0] = request; + journals[0] = APP_JOURNAL; + + // Second request + client = getClient(1); + request = client.request(uint32(1)); + + request.requirements = Requirements({ + predicate: PredicateLibrary.createDigestMatchPredicate(bytes32(APP_IMAGE_ID_2), sha256(APP_JOURNAL_2)), + selector: bytes4(0), + callback: Callback({addr: address(0), gasLimit: 0}) + }); + boundlessMarket.lockRequestWithSignature( + request, client.sign(request), testProver.signLockRequest(LockRequest({request: request})) + ); + requests[1] = request; + journals[1] = APP_JOURNAL_2; + + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) = + createFillsAndSubmitRoot(requests, journals, testProverAddress); + + bytes memory fulfillmentData0 = fills[0].fulfillmentData; + bytes32 claimDigest0 = fills[0].claimDigest; + + fills[0].fulfillmentData = fills[1].fulfillmentData; + fills[1].fulfillmentData = fulfillmentData0; + + fills[0].claimDigest = fills[1].claimDigest; + fills[1].claimDigest = claimDigest0; + + vm.expectRevert(VerificationFailed.selector); + boundlessMarket.fulfill(fills, assessorReceipt); + + expectMarketBalanceUnchanged(); + } + + // Test that a smart contract signature can be used to price a request. + // The smart contract signature must be validated when a request is priced. This + // ensures that the smart contract signature is checked in the never locked path, + // since the signature is not checked at lock time (nor in the assessor). + function testPriceRequestSmartContractSignature() external { + SmartContractClient client = getSmartContractClient(1); + ProofRequest memory request = client.request(3); + bytes memory clientSignature = client.sign(request); + + // Expect isValidSignature to be called on the smart contract wallet + bytes32 requestHash = + MessageHashUtils.toTypedDataHash(boundlessMarket.eip712DomainSeparator(), request.eip712Digest()); + vm.expectCall( + client.addr(), abi.encodeWithSelector(IERC1271.isValidSignature.selector, requestHash, clientSignature) + ); + boundlessMarket.priceRequest(request, clientSignature); + } + + function testPriceRequestSmartContractSignatureExceedsGasLimit() external { + SmartContractClient client = getSmartContractClient(1); + client.smartWallet().setGasCost(boundlessMarket.ERC1271_MAX_GAS_FOR_CHECK() + 1); + ProofRequest memory request = client.request(3); + bytes memory clientSignature = client.sign(request); + + // Expect isValidSignature to be called on the smart contract wallet + bytes32 requestHash = + MessageHashUtils.toTypedDataHash(boundlessMarket.eip712DomainSeparator(), request.eip712Digest()); + vm.expectCall( + client.addr(), abi.encodeWithSelector(IERC1271.isValidSignature.selector, requestHash, clientSignature) + ); + vm.expectRevert(bytes("")); // revert due to out of gas results in empty error + boundlessMarket.priceRequest(request, clientSignature); + } + + // Test that a smart contract signature can be used to price and fulfill a request. + // The smart contract signature must be validated when a request is priced. This + // ensures that the smart contract signature is validated during the never locked path, + // since the signature is not checked at lock time (nor in the assessor). + function testPriceAndFulfillSmartContractSignature() external { + SmartContractClient client = getSmartContractClient(1); + ProofRequest memory request = client.request(3); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + // Expect isValidSignature to be called on the smart contract wallet + bytes32 requestHash = + MessageHashUtils.toTypedDataHash(boundlessMarket.eip712DomainSeparator(), request.eip712Digest()); + vm.expectCall( + client.addr(), abi.encodeWithSelector(IERC1271.isValidSignature.selector, requestHash, clientSignature) + ); + + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + vm.snapshotGasLastCall("priceAndFulfill: a single request (smart contract signature)"); + + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } + + // Fulfill a batch of locked requests and withdraw + function testFulfillAndWithdrawLockedRequests() public { + // Provide a batch definition as an array of clients and how many requests each submits. + uint256[5] memory batch = [uint256(1), 2, 1, 3, 1]; + uint256 batchSize = 0; + for (uint256 i = 0; i < batch.length; i++) { + batchSize += batch[i]; + } + + ProofRequest[] memory requests = new ProofRequest[](batchSize); + bytes[] memory journals = new bytes[](batchSize); + uint256 expectedRevenue = 0; + uint256 idx = 0; + for (uint256 i = 0; i < batch.length; i++) { + Client client = getClient(i); + + for (uint256 j = 0; j < batch[i]; j++) { + ProofRequest memory request = client.request(uint32(j)); + + // TODO: This is a fragile part of this test. It should be improved. + uint256 desiredPrice = uint256(1.5 ether); + vm.warp(request.offer.timeAtPrice(desiredPrice)); + expectedRevenue += desiredPrice; + + boundlessMarket.lockRequestWithSignature( + request, client.sign(request), testProver.signLockRequest(LockRequest({request: request})) + ); + + requests[idx] = request; + journals[idx] = APP_JOURNAL; + idx++; + } + } + + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) = + createFillsAndSubmitRoot(requests, journals, testProverAddress); + + uint256 initialBalance = testProverAddress.balance + boundlessMarket.balanceOf(testProverAddress); + + for (uint256 i = 0; i < fills.length; i++) { + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(fills[i].id, testProverAddress, fills[i].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(fills[i].id, testProverAddress, fills[i]); + } + boundlessMarket.fulfillAndWithdraw(fills, assessorReceipt); + vm.snapshotGasLastCall(string.concat("fulfillAndWithdraw: a batch of ", vm.toString(batchSize))); + + for (uint256 i = 0; i < fills.length; i++) { + // Check that the proof was submitted + expectRequestFulfilled(fills[i].id); + } + + assert(boundlessMarket.balanceOf(testProverAddress) == 0); + assert(testProverAddress.balance == initialBalance + uint256(expectedRevenue)); + } + + function testPriceAndFulfillLockedRequest() external { + Client client = getClient(1); + ProofRequest memory request = client.request(3); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = client.sign(request); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + vm.snapshotGasLastCall("priceAndFulfill: a single request"); + + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } + + function testSubmitRootAndPriceAndFulfillLockedRequest() external { + Client client = getClient(1); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = client.request(3); + bytes[] memory journals = new bytes[](1); + journals[0] = APP_JOURNAL; + + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) = + createFills(requests, journals, testProverAddress); + + bytes memory seal = + verifier.mockProve( + SET_BUILDER_IMAGE_ID, sha256(abi.encodePacked(SET_BUILDER_IMAGE_ID, uint256(1 << 255), root)) + ) + .seal; + + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = client.sign(requests[0]); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(requests[0].id, testProverAddress, fills[0].requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(requests[0].id, testProverAddress, fills[0]); + boundlessMarket.submitRootAndPriceAndFulfill( + address(setVerifier), root, seal, requests, clientSignatures, fills, assessorReceipt + ); + vm.snapshotGasLastCall("submitRootAndPriceAndFulfill: a single request"); + + expectRequestFulfilled(fills[0].id); + + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } + + function _testFulfillAlreadyFulfilled(uint32 idx, LockRequestMethod lockinMethod) private { + (, ProofRequest memory request) = _testFulfillSameBlock(idx, lockinMethod); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = getClient(1).sign(request); + + // TODO(#704): Workaround in test for edge case described in #704 + vm.warp(request.offer.lockDeadline() + 1); + + // Attempt to fulfill a request already fulfilled + // should return "RequestIsFulfilled({requestId: request.id})" + bytes[] memory paymentError = + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + assert( + keccak256(paymentError[0]) + == keccak256(abi.encodeWithSelector(IBoundlessMarket.RequestIsFulfilled.selector, request.id)) + ); + + expectMarketBalanceUnchanged(); + } + + function testPriceAndFulfillWithSelector() external { + Client client = getClient(1); + ProofRequest memory request = client.request(3); + request.requirements.selector = setVerifier.SELECTOR(); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = client.sign(request); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + vm.snapshotGasLastCall("priceAndFulfill: a single request (with selector)"); + + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } + + function testFulfillRequestWrongSelector() public { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + request.requirements.selector = setVerifier.SELECTOR(); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + // Attempt to fulfill a request with wrong selector. + assessorReceipt.selectors[0] = Selector({index: 0, value: bytes4(0xdeadbeef)}); + vm.expectRevert( + abi.encodeWithSelector( + IBoundlessMarket.SelectorMismatch.selector, bytes4(0xdeadbeef), setVerifier.SELECTOR() + ) + ); + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + expectMarketBalanceUnchanged(); + } + + function testFulfillApplicationVerificationGasLimit() public { + Client client = getClient(1); + ProofRequest memory request = client.request(3); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + FulfillmentDataImageIdAndJournal memory fulfillmentData = + FulfillmentDataLibrary.decodeFulfillmentDataImageIdAndJournal(fill.fulfillmentData); + bytes32 claimDigest = ReceiptClaimLib.ok(fulfillmentData.imageId, sha256(fulfillmentData.journal)).digest(); + + // If no selector is specified, we expect the call to verifyIntegrity to use the default + // gas limit when verifying the application. + vm.expectCall( + address(setVerifier), + 0, + uint64(EXPECTED_DEFAULT_MAX_GAS_FOR_VERIFY), + abi.encodeWithSelector(IRiscZeroVerifier.verifyIntegrity.selector, RiscZeroReceipt(fill.seal, claimDigest)) + ); + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } + + function testFulfillVerificationGasLimitForSelector() public { + Client client = getClient(1); + ProofRequest memory request = client.request(3); + request.requirements.selector = setVerifier.SELECTOR(); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + FulfillmentDataImageIdAndJournal memory fulfillmentData = + FulfillmentDataLibrary.decodeFulfillmentDataImageIdAndJournal(fill.fulfillmentData); + bytes32 claimDigest = ReceiptClaimLib.ok(fulfillmentData.imageId, sha256(fulfillmentData.journal)).digest(); + + // If a selector is specified, we expect the call to verifyIntegrity to not use the default + // gas limit, so the minimum gas it should have should exceed it. + vm.expectCallMinGas( + address(setVerifier), + 0, + uint64(EXPECTED_DEFAULT_MAX_GAS_FOR_VERIFY + 1), + abi.encodeWithSelector(IRiscZeroVerifier.verifyIntegrity.selector, RiscZeroReceipt(fill.seal, claimDigest)) + ); + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + expectRequestFulfilled(fill.id); + + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } + + function _testFulfillRepeatIndex(LockRequestMethod lockinMethod) private { + Client client = getClient(1); + + // Create two distinct requests with the same ID. It should be the case that only one can be + // filled, and if one is locked, the other cannot be filled. + Offer memory offerA = client.defaultOffer(); + Offer memory offerB = client.defaultOffer(); + offerB.maxPrice = 3 ether; + ProofRequest memory requestA = client.request(1, offerA); + ProofRequest memory requestB = client.request(1, offerB); + bytes memory clientSignatureA = client.sign(requestA); + + // Lock-in request A. + if (lockinMethod == LockRequestMethod.LockRequest) { + vm.prank(testProverAddress); + boundlessMarket.lockRequest(requestA, clientSignatureA); + } else if (lockinMethod == LockRequestMethod.LockRequestWithSig) { + boundlessMarket.lockRequestWithSignature( + requestA, clientSignatureA, testProver.signLockRequest(LockRequest({request: requestA})) + ); + } + + client.snapshotBalance(); + testProver.snapshotBalance(); + + // Attempt to fill request B. + (Fulfillment memory fillB, AssessorReceipt memory assessorReceiptB) = + createFillAndSubmitRoot(requestB, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fillsB = new Fulfillment[](1); + fillsB[0] = fillB; + + if (lockinMethod == LockRequestMethod.None) { + // Annoying boilerplate for creating singleton lists. + // Here we price/lock with request A and try to fill with request B. + ProofRequest[] memory requestsA = new ProofRequest[](1); + requestsA[0] = requestA; + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignatureA; + + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.RequestIsNotLockedOrPriced.selector, requestA.id)); + boundlessMarket.priceAndFulfill(requestsA, clientSignatures, fillsB, assessorReceiptB); + + expectRequestNotFulfilled(fillB.id); + } else { + // Attempting to fulfill request B should revert, since it has never been seen onchain. + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.RequestIsNotLockedOrPriced.selector, requestA.id)); + boundlessMarket.fulfill(fillsB, assessorReceiptB); + expectRequestNotFulfilled(fillB.id); + + // Attempting to price and fulfill with request B should return a + // payment error since request A is still locked. + ProofRequest[] memory requestsB = new ProofRequest[](1); + requestsB[0] = requestB; + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = client.sign(requestB); + + bytes[] memory paymentErrors = + boundlessMarket.priceAndFulfill(requestsB, clientSignatures, fillsB, assessorReceiptB); + assert( + keccak256(paymentErrors[0]) + == keccak256(abi.encodeWithSelector(IBoundlessMarket.RequestIsLocked.selector, requestB.id)) + ); + expectRequestFulfilled(fillB.id); + } + + // No balance changes should have occurred after lockin. + client.expectBalanceChange(0 ether); + testProver.expectBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + } + + function testSubmitRootAndFulfill() public { + (ProofRequest[] memory requests, bytes[] memory journals) = newBatch(2); + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt, bytes32 root) = + createFills(requests, journals, testProverAddress); + + bytes memory seal = + verifier.mockProve( + SET_BUILDER_IMAGE_ID, sha256(abi.encodePacked(SET_BUILDER_IMAGE_ID, uint256(1 << 255), root)) + ) + .seal; + boundlessMarket.submitRootAndFulfill(address(setVerifier), root, seal, fills, assessorReceipt); + vm.snapshotGasLastCall("submitRootAndFulfill: a batch of 2 requests"); + + for (uint256 j = 0; j < fills.length; j++) { + expectRequestFulfilled(fills[j].id); + } + } + + function testSlashLockedRequestFullyExpired() public returns (Client, ProofRequest memory) { + (Client client, ProofRequest memory request) = testFulfillLockedRequestFullyExpired(); + // Provers stake balance is subtracted at lock time, not when slash is called + testProver.expectCollateralBalanceChange(-uint256(request.offer.lockCollateral).toInt256()); + + snapshotMarketCollateralBalance(); + snapshotMarketStakeTreasuryBalance(); + + // Slash the request + // Burning = sending tokens to address 0xdEaD, expect a transfer event to be emitted to address 0xdEaD + vm.expectEmit(true, true, true, false); + emit IERC20.Transfer(address(proxy), address(0xdEaD), request.offer.lockCollateral); + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.ProverSlashed( + request.id, + expectedSlashBurnAmount(request.offer.lockCollateral), + expectedSlashTransferAmount(request.offer.lockCollateral), + address(boundlessMarket) + ); + + boundlessMarket.slash(request.id); + vm.snapshotGasLastCall("slash: base case"); + + expectMarketCollateralBalanceChange(-int256(int96(expectedSlashBurnAmount(request.offer.lockCollateral)))); + expectMarketCollateralTreasuryBalanceChange( + int256(int96(expectedSlashTransferAmount(request.offer.lockCollateral))) + ); + + client.expectBalanceChange(0 ether); + testProver.expectCollateralBalanceChange(-uint256(request.offer.lockCollateral).toInt256()); + + // Check that the request is slashed and is not fulfilled + expectRequestSlashed(request.id); + + return (client, request); + } + + // Prover locks a request, the request expires, then they fulfill a request with the same ID. + // Prover should be slashable, but still able to fulfill the other request and receive payment for it. + function testSlashLockedRequestMultipleRequestsSameIndex() public { + Client client = getClient(1); + + // Create two distinct requests with the same ID. + Offer memory offerA = Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(10), + lockTimeout: uint32(100), + timeout: uint32(100), + lockCollateral: 1 ether + }); + Offer memory offerB = Offer({ + minPrice: 3 ether, + maxPrice: 3 ether, + rampUpStart: uint64(block.timestamp) + uint64(offerA.timeout) + 1, + rampUpPeriod: uint32(10), + lockTimeout: uint32(100), + timeout: 100, + lockCollateral: 1 ether + }); + ProofRequest memory requestA = client.request(1, offerA); + ProofRequest memory requestB = client.request(1, offerB); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = requestB; + bytes memory clientSignatureA = client.sign(requestA); + bytes memory clientSignatureB = client.sign(requestB); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignatureB; + + client.snapshotBalance(); + testProver.snapshotBalance(); + + vm.prank(testProverAddress); + boundlessMarket.lockRequest(requestA, clientSignatureA); + + vm.warp(requestA.offer.deadline() + 1); + + // Attempt to fill request B. + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(requestB, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + boundlessMarket.slash(requestA.id); + + expectRequestFulfilledAndSlashed(fill.id); + + client.expectBalanceChange(-3 ether); + testProver.expectBalanceChange(3 ether); + // They lose their original stake, but gain a portion of the slashed stake. + testProver.expectCollateralBalanceChange( + -1 ether + int256(uint256(expectedSlashTransferAmount(requestA.offer.lockCollateral))) + ); + expectMarketBalanceUnchanged(); + } + + // Handles case where a third-party that was not locked fulfills the request, and the locked prover does not. + // Once the locked prover is slashed, we expect the request to be both "fulfilled" and "slashed". + // We expect a portion of slashed funds to go to the market treasury. + function testSlashLockedRequestFulfilledByOtherProverDuringLock() public { + Client client = getClient(1); + ProofRequest memory request = client.request(1); + + // Lock to "testProver" but "prover2" fulfills the request + boundlessMarket.lockRequestWithSignature( + request, client.sign(request), testProver.signLockRequest(LockRequest({request: request})) + ); + + Client testProver2 = getClient(2); + (address testProver2Address,,,) = testProver2.wallet(); + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProver2Address); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + boundlessMarket.fulfill(fills, assessorReceipt); + expectRequestFulfilled(fill.id); + + vm.warp(request.offer.deadline() + 1); + + // Slash the original prover that locked and didnt deliver + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.ProverSlashed( + request.id, + expectedSlashBurnAmount(request.offer.lockCollateral), + expectedSlashTransferAmount(request.offer.lockCollateral), + address(boundlessMarket) + ); + boundlessMarket.slash(request.id); + + client.expectBalanceChange(0 ether); + testProver.expectCollateralBalanceChange(-uint256(request.offer.lockCollateral).toInt256()); + testProver2.expectCollateralBalanceChange(0 ether); + + // We expect the request is both slashed and fulfilled + require(boundlessMarket.requestIsSlashed(request.id), "Request should be slashed"); + require(boundlessMarket.requestIsFulfilled(request.id), "Request should be fulfilled"); + } + + function testSlashInvalidRequestID() public { + // Attempt to slash an invalid request ID + // should revert with "RequestIsNotLocked({requestId: request.id})" + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.RequestIsNotLocked.selector, 0xa)); + boundlessMarket.slash(RequestId.wrap(0xa)); + + expectMarketBalanceUnchanged(); + } + + function testSlashLockedRequestNotExpired() public { + (, ProofRequest memory request) = testLockRequest(); + + // Attempt to slash a request not expired + // should revert with "RequestIsNotExpired({requestId: request.id, deadline: deadline})" + vm.expectRevert( + abi.encodeWithSelector(IBoundlessMarket.RequestIsNotExpired.selector, request.id, request.offer.deadline()) + ); + boundlessMarket.slash(request.id); + + expectMarketBalanceUnchanged(); + } + + // Even if the lock has expired, you can not slash until the request is fully expired, as we need to know if the + // request was eventually fulfilled or not to decide who to send stake to. + function testSlashWasLockedRequestNotFullyExpired() public { + Client client = getClient(1); + ProofRequest memory request = client.request( + 1, + Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(50), + lockTimeout: uint32(50), + timeout: uint32(100), + lockCollateral: 1 ether + }) + ); + bytes memory clientSignature = client.sign(request); + + Client locker = getProver(1); + client.snapshotBalance(); + locker.snapshotBalance(); + + address lockerAddress = locker.addr(); + vm.prank(lockerAddress); + boundlessMarket.lockRequest(request, clientSignature); + // At this point the client should have only been charged the 1 ETH at lock time. + client.expectBalanceChange(-1 ether); + + // Advance the chain ahead to simulate the lock timeout. + vm.warp(request.offer.lockDeadline() + 1); + + // Attempt to slash a request not expired + // should revert with "RequestIsNotExpired({requestId: request.id, deadline: deadline})" + vm.expectRevert( + abi.encodeWithSelector(IBoundlessMarket.RequestIsNotExpired.selector, request.id, request.offer.deadline()) + ); + boundlessMarket.slash(request.id); + + expectMarketBalanceUnchanged(); + } + + function _testSlashFulfilledSameBlock(uint32 idx, LockRequestMethod lockinMethod) private { + (, ProofRequest memory request) = _testFulfillSameBlock(idx, lockinMethod); + + if (lockinMethod == LockRequestMethod.None) { + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.RequestIsNotLocked.selector, request.id)); + } else { + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.RequestIsFulfilled.selector, request.id)); + } + + boundlessMarket.slash(request.id); + + expectMarketBalanceUnchanged(); + } + + function testSlashLockedRequestFulfilledByLocker() public { + _testSlashFulfilledSameBlock(1, LockRequestMethod.LockRequest); + _testSlashFulfilledSameBlock(2, LockRequestMethod.LockRequestWithSig); + } + + function testSlashNeverLockedRequestFulfilled() public { + _testSlashFulfilledSameBlock(3, LockRequestMethod.None); + } + + // Test slashing in the scenario where a request is fulfilled by another prover after the lock expires. + // but before the request as a whole has expired. + function testSlashWasLockedRequestFulfilledByOtherProver() + public + returns (ProofRequest memory, Client, Client, Client) + { + snapshotMarketStakeTreasuryBalance(); + (ProofRequest memory request, Client client, Client locker, Client otherProver) = + testFulfillWasLockedRequestByOtherProver(); + vm.warp(request.offer.deadline() + 1); + otherProver.snapshotCollateralBalance(); + + // We expect the prover that ultimately fulfilled the request to receive stake. + // Burning = sending tokens to address 0xdEaD, expect a transfer event to be emitted to address 0xdEaD + vm.expectEmit(true, true, true, false); + emit IERC20.Transfer(address(proxy), address(0xdEaD), request.offer.lockCollateral); + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.ProverSlashed( + request.id, + expectedSlashBurnAmount(request.offer.lockCollateral), + expectedSlashTransferAmount(request.offer.lockCollateral), + otherProver.addr() + ); + + boundlessMarket.slash(request.id); + vm.snapshotGasLastCall("slash: fulfilled request after lock deadline"); + + // Prover should have their original balance less the stake amount. + testProver.expectCollateralBalanceChange(-uint256(request.offer.lockCollateral).toInt256()); + // Other prover should receive a portion of the stake + otherProver.expectCollateralBalanceChange( + uint256(expectedSlashTransferAmount(request.offer.lockCollateral)).toInt256() + ); + + expectMarketCollateralTreasuryBalanceChange(0); + expectMarketBalanceUnchanged(); + + return (request, client, locker, otherProver); + } + + // In this case the lock expires, the request is fulfilled by another prover, the request is slashed, + // and then finally the locker tries to fulfill the request. + // + // In this case the request has fully expired, so the proof should NOT be delivered, + // however we should not revert (as this allows partial fulfillment of other requests in the batch). + function testSlashWasLockedRequestFulfilledByOtherProverFulfillAfterRequestExpired() public { + (ProofRequest memory request, Client client, Client locker,) = testSlashWasLockedRequestFulfilledByOtherProver(); + vm.warp(request.offer.deadline() + 1); + + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + // Advance the chain ahead to simulate the request expiration. + vm.warp(request.offer.deadline() + 1); + + // The locker should have no balance change. + // Now the locker tries to fulfill the request. + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, locker.addr()); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + // In this case the request has fully expired, so the proof should NOT be delivered, + // however we should not revert (as this allows partial fulfillment of other requests in the batch) + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsExpired.selector, request.id + )); + + // The fulfillment should not revert, as we support multiple proofs being delivered for a single request. + bytes[] memory paymentErrors = + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + assert( + keccak256(paymentErrors[0]) + == keccak256(abi.encodeWithSelector(IBoundlessMarket.RequestIsExpired.selector, request.id)) + ); + } + + // Test slashing in the scenario where a request is fulfilled by the locker after the lock expires. + // but before the request as a whole has expired. + function testSlashWasLockedRequestFulfilledByLocker() public { + snapshotMarketStakeTreasuryBalance(); + (ProofRequest memory request, Client prover) = testFulfillWasLockedRequestByOriginalLocker(); + vm.warp(request.offer.deadline() + 1); + + // We expect the prover that ultimately fulfilled the request to receive stake. + // Burning = sending tokens to address 0xdEaD, expect a transfer event to be emitted to address 0xdEaD + vm.expectEmit(true, true, true, false); + emit IERC20.Transfer(address(proxy), address(0xdEaD), request.offer.lockCollateral); + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.ProverSlashed( + request.id, + expectedSlashBurnAmount(request.offer.lockCollateral), + expectedSlashTransferAmount(request.offer.lockCollateral), + prover.addr() + ); + + boundlessMarket.slash(request.id); + + // Prover should have their original balance less the stake amount plus the stake for eventually filling. + prover.expectCollateralBalanceChange( + -uint256(request.offer.lockCollateral).toInt256() + + uint256(expectedSlashTransferAmount(request.offer.lockCollateral)).toInt256() + ); + + expectMarketCollateralTreasuryBalanceChange(0); + expectMarketBalanceUnchanged(); + } + + function testSlashSlash() public { + (, ProofRequest memory request) = testSlashLockedRequestFullyExpired(); + expectRequestSlashed(request.id); + + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.RequestIsSlashed.selector, request.id)); + boundlessMarket.slash(request.id); + } + + function testLockRequestSmartContractSignature() public { + SmartContractClient client = getSmartContractClient(1); + ProofRequest memory request = client.request(1); + bytes memory clientSig = client.sign(request); + + // Expect isValidSignature to be called on the smart contract wallet + bytes32 requestHash = + MessageHashUtils.toTypedDataHash(boundlessMarket.eip712DomainSeparator(), request.eip712Digest()); + vm.expectCall(client.addr(), abi.encodeWithSelector(IERC1271.isValidSignature.selector, requestHash, clientSig)); + + // Call lockRequest with the smart contract signature + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSig); + + // Verify the lock request + assertTrue(boundlessMarket.requestIsLocked(request.id), "Request should be locked"); + } + + // Test that the smart contract client receives the proof request when isValidSignature is called, + // if the client signature provided is empty. This enables custom smart contract clients that want to authorize + // payments based on how a proof request is structured. + function testLockRequestSmartContractClientValidatesPassthroughEmptySignature() public { + SmartContractClient client = getSmartContractClient(1); + ProofRequest memory request = client.request(1); + bytes memory clientSig = bytes(""); + client.setExpectedSignature(clientSig); + + // Expect isValidSignature to be called on the smart contract wallet with the proof request as the signature. + bytes32 requestHash = + MessageHashUtils.toTypedDataHash(boundlessMarket.eip712DomainSeparator(), request.eip712Digest()); + vm.expectCall(client.addr(), abi.encodeWithSelector(IERC1271.isValidSignature.selector, requestHash, clientSig)); + + // Call lockRequest with the smart contract signature + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSig); + + // Verify the lock request + assertTrue(boundlessMarket.requestIsLocked(request.id), "Request should be locked"); + } + + function testLockRequestSmartContractSignatureInvalid() public { + SmartContractClient client = getSmartContractClient(1); + ProofRequest memory request = client.request(1); + bytes memory clientSig = bytes("invalid_signature"); + + // Expect isValidSignature to be called on the smart contract wallet + bytes32 requestHash = + MessageHashUtils.toTypedDataHash(boundlessMarket.eip712DomainSeparator(), request.eip712Digest()); + vm.expectCall(client.addr(), abi.encodeWithSelector(IERC1271.isValidSignature.selector, requestHash, clientSig)); + + // Call lockRequest with the smart contract signature + vm.prank(testProverAddress); + vm.expectRevert(IBoundlessMarket.InvalidSignature.selector); + boundlessMarket.lockRequest(request, clientSig); + } + + function testLockRequestSmartContractSignatureExceedsGasLimit() public { + SmartContractClient client = getSmartContractClient(1); + client.smartWallet().setGasCost(boundlessMarket.ERC1271_MAX_GAS_FOR_CHECK() + 1); + ProofRequest memory request = client.request(1); + bytes memory clientSig = client.sign(request); + + // Expect isValidSignature to be called on the smart contract wallet + bytes32 requestHash = + MessageHashUtils.toTypedDataHash(boundlessMarket.eip712DomainSeparator(), request.eip712Digest()); + vm.expectCall(client.addr(), abi.encodeWithSelector(IERC1271.isValidSignature.selector, requestHash, clientSig)); + + // Call lockRequest with the smart contract signature + vm.prank(testProverAddress); + vm.expectRevert(bytes("")); // revert due to out of gas results in empty error + boundlessMarket.lockRequest(request, clientSig); + } + + function testLockRequestWithSignatureClientSmartContractSignatureInvalid() public { + SmartContractClient client = getSmartContractClient(1); + Client prover = getClient(2); + + ProofRequest memory request = client.request(1); + bytes memory clientSig = bytes("invalid_signature"); + bytes memory proverSig = prover.signLockRequest(LockRequest({request: request})); + + address proverAddress = prover.addr(); + vm.prank(proverAddress); + vm.expectRevert(IBoundlessMarket.InvalidSignature.selector); + boundlessMarket.lockRequestWithSignature(request, clientSig, proverSig); + } + + function testFulfillLockedRequestWithCallback() public { + Client client = getClient(1); + + // Create request with low gas callback + ProofRequest memory request = client.request(1); + request.requirements.callback = Callback({addr: address(mockCallback), gasLimit: 500_000}); + + bytes memory clientSignature = client.sign(request); + client.snapshotBalance(); + testProver.snapshotBalance(); + + // Lock and fulfill the request + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + vm.expectEmit(true, true, true, false); + bytes32 imageId = bytesToBytes32(request.requirements.predicate.data); + emit MockCallback.MockCallbackCalled(imageId, APP_JOURNAL, fill.seal); + boundlessMarket.fulfill(fills, assessorReceipt); + + // Verify callback was called exactly once + assertEq(mockCallback.getCallCount(), 1, "Callback should be called exactly once"); + + // Verify request state and balances + expectRequestFulfilled(fill.id); + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } + + function testFulfillLockedRequestWithCallbackNotEnoughGas() public { + Client client = getClient(1); + + // Create request with low gas callback + ProofRequest memory request = client.request(1); + request.requirements.callback = Callback({addr: address(mockCallback), gasLimit: 500_000}); + + bytes memory clientSignature = client.sign(request); + client.snapshotBalance(); + testProver.snapshotBalance(); + + // Lock and fulfill the request + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectRevert(IBoundlessMarket.InsufficientGas.selector); + boundlessMarket.fulfill{gas: 499_000}(fills, assessorReceipt); + + // Verify callback was not called + assertEq(mockCallback.getCallCount(), 0, "Callback should not be called"); + + expectRequestNotFulfilled(request.id); + expectMarketBalanceUnchanged(); + } + + function testFulfillLockedRequestWithCallbackExceedGasLimit() public { + Client client = getClient(1); + + // Create request with high gas callback that will exceed limit + ProofRequest memory request = client.request(1); + request.requirements.callback = Callback({addr: address(mockHighGasCallback), gasLimit: 10_000}); + + bytes memory clientSignature = client.sign(request); + client.snapshotBalance(); + testProver.snapshotBalance(); + + // Lock and fulfill the request + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.CallbackFailed(request.id, address(mockHighGasCallback), ""); + boundlessMarket.fulfill(fills, assessorReceipt); + + // Verify callback was attempted + assertEq(mockHighGasCallback.getCallCount(), 0, "Callback not succeed"); + + // Verify request state and balances + expectRequestFulfilled(fill.id); + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } + + function testFulfillLockedRequestWithCallbackByOtherProver() public { + Client client = getClient(1); + + // Create request with low gas callback + ProofRequest memory request = client.request(1); + request.requirements.callback = Callback({addr: address(mockCallback), gasLimit: 100_000}); + + bytes memory clientSignature = client.sign(request); + + // Lock request with testProver + boundlessMarket.lockRequestWithSignature( + request, clientSignature, testProver.signLockRequest(LockRequest({request: request})) + ); + + // Have otherProver fulfill without requiring payment + Client otherProver = getProver(2); + address otherProverAddress = otherProver.addr(); + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, otherProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, otherProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsLocked.selector, request.id + )); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, otherProverAddress, fill); + vm.expectEmit(true, true, true, true); + bytes32 imageId = bytesToBytes32(request.requirements.predicate.data); + emit MockCallback.MockCallbackCalled(imageId, APP_JOURNAL, fill.seal); + + vm.prank(otherProverAddress); + boundlessMarket.fulfill(fills, assessorReceipt); + + // Verify callback was called exactly once + assertEq(mockCallback.getCallCount(), 1, "Callback should be called exactly once"); + + // Verify request state and balances + expectRequestFulfilled(fill.id); + testProver.expectCollateralBalanceChange(-int256(uint256(request.offer.lockCollateral))); + otherProver.expectBalanceChange(0); + otherProver.expectCollateralBalanceChange(0); + expectMarketBalanceUnchanged(); + } + + function testFulfillLockedRequestWithCallbackAlreadyFulfilledByOtherProver() public { + Client client = getClient(1); + + ProofRequest memory request = client.request(1); + request.requirements.callback = Callback({addr: address(mockCallback), gasLimit: 100_000}); + + bytes memory clientSignature = client.sign(request); + + // Lock request with testProver + boundlessMarket.lockRequestWithSignature( + request, clientSignature, testProver.signLockRequest(LockRequest({request: request})) + ); + + // Have otherProver fulfill without requiring payment + Client otherProver = getProver(2); + address otherProverAddress = address(otherProver); + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, otherProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, otherProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.PaymentRequirementsFailed(abi.encodeWithSelector( + IBoundlessMarket.RequestIsLocked.selector, request.id + )); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, otherProverAddress, fill); + vm.expectEmit(true, true, true, true); + bytes32 imageId = bytesToBytes32(request.requirements.predicate.data); + emit MockCallback.MockCallbackCalled(imageId, APP_JOURNAL, fill.seal); + boundlessMarket.fulfill(fills, assessorReceipt); + + // Verify callback was called exactly once + assertEq(mockCallback.getCallCount(), 1, "Callback should be called exactly once"); + + // Now have original locker fulfill to get payment + (fill, assessorReceipt) = createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress); + fills[0] = fill; + boundlessMarket.fulfill(fills, assessorReceipt); + + // Verify callback is called again + assertEq(mockCallback.getCallCount(), 2, "Callback should be called twice"); + + expectRequestFulfilled(fill.id); + testProver.expectBalanceChange(1 ether); + testProver.expectCollateralBalanceChange(0 ether); + otherProver.expectBalanceChange(0); + otherProver.expectCollateralBalanceChange(0); + expectMarketBalanceUnchanged(); + } + + function testFulfillWasLockedRequestWithCallbackByOtherProver() public { + Client client = getClient(1); + + // Create request with lock timeout of 50 blocks, overall timeout of 100 + ProofRequest memory request = client.request( + 1, + Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(50), + lockTimeout: uint32(50), + timeout: uint32(100), + lockCollateral: 1 ether + }) + ); + request.requirements.callback = Callback({addr: address(mockCallback), gasLimit: 100_000}); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + + bytes memory clientSignature = client.sign(request); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignature; + + Client locker = getProver(1); + Client otherProver = getProver(2); + + client.snapshotBalance(); + locker.snapshotBalance(); + otherProver.snapshotBalance(); + + address lockerAddress = locker.addr(); + vm.prank(lockerAddress); + boundlessMarket.lockRequest(request, clientSignature); + client.expectBalanceChange(-1 ether); + + // Advance chain ahead to simulate lock timeout + vm.warp(request.offer.lockDeadline() + 1); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, otherProver.addr()); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, otherProver.addr(), fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, otherProver.addr(), fill); + vm.expectEmit(true, true, true, true); + bytes32 imageId = bytesToBytes32(request.requirements.predicate.data); + emit MockCallback.MockCallbackCalled(imageId, APP_JOURNAL, fill.seal); + boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + + // Verify callback was called exactly once + assertEq(mockCallback.getCallCount(), 1, "Callback should be called exactly once"); + + // Check request state and balances + expectRequestFulfilled(fill.id); + client.expectBalanceChange(0 ether); + locker.expectBalanceChange(0 ether); + locker.expectCollateralBalanceChange(-1 ether); + otherProver.expectBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + } + + function testFulfillWasLockedRequestWithCallbackMultipleRequestsSameIndex() public { + Client client = getClient(1); + + // Create first request with callback A + Offer memory offerA = Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(10), + lockTimeout: uint32(100), + timeout: uint32(100), + lockCollateral: 1 ether + }); + ProofRequest memory requestA = client.request(1, offerA); + requestA.requirements.callback = Callback({addr: address(mockCallback), gasLimit: 10_000}); + bytes memory clientSignatureA = client.sign(requestA); + + // Create second request with same ID but different callback + Offer memory offerB = Offer({ + minPrice: 1 ether, + maxPrice: 3 ether, + rampUpStart: offerA.rampUpStart, + rampUpPeriod: offerA.rampUpPeriod, + lockTimeout: offerA.lockTimeout + 100, + timeout: offerA.timeout + 100, + lockCollateral: offerA.lockCollateral + }); + ProofRequest memory requestB = client.request(1, offerB); + requestB.requirements.callback = Callback({addr: address(mockHighGasCallback), gasLimit: 300_000}); + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = requestB; + bytes memory clientSignatureB = client.sign(requestB); + bytes[] memory clientSignatures = new bytes[](1); + clientSignatures[0] = clientSignatureB; + + client.snapshotBalance(); + testProver.snapshotBalance(); + + // Withdraw some funds so we only have funds to cover for the first offer + // and we have a deficit for the second offer to test the partial payment path + vm.prank(client.addr()); + boundlessMarket.withdraw(DEFAULT_BALANCE - 2 ether); + + // Lock request A + vm.prank(testProverAddress); + boundlessMarket.lockRequest(requestA, clientSignatureA); + + // Advance chain ahead to simulate request A lock timeout + vm.warp(requestA.offer.lockDeadline() + 1); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(requestB, APP_JOURNAL, testProverAddress); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + // Since the request being fulfilled is distinct from the one that was locked, the + // transaction should revert if the request is not priced before fulfillment. + vm.expectRevert(abi.encodeWithSelector(IBoundlessMarket.RequestIsNotLockedOrPriced.selector, requestB.id)); + boundlessMarket.fulfill(fills, assessorReceipt); + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(requestB.id, testProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(requestB.id, testProverAddress, fill); + vm.expectEmit(true, true, true, true); + bytes32 imageId = bytesToBytes32(requestB.requirements.predicate.data); + emit MockCallback.MockCallbackCalled(imageId, APP_JOURNAL, fill.seal); + bytes[] memory errors = boundlessMarket.priceAndFulfill(requests, clientSignatures, fills, assessorReceipt); + // Verify that the second request was partially payed + assertEq(errors.length, 1, "Expected one error"); + assertEq( + errors[0], + abi.encodeWithSelector(IBoundlessMarket.PartialPayment.selector, 3 ether, 2 ether), + "Unexpected error" + ); + + // Verify only the second request's callback was called + assertEq(mockCallback.getCallCount(), 0, "First request's callback should not be called"); + assertEq(mockHighGasCallback.getCallCount(), 1, "Second request's callback should be called once"); + + // Deposit back original funds so that the Market original balance is restored + vm.prank(client.addr()); + boundlessMarket.deposit{value: DEFAULT_BALANCE - 2 ether}(); + + // Verify request state and balances + expectRequestFulfilled(fill.id); + client.expectBalanceChange(-2 ether); + testProver.expectBalanceChange(2 ether); + testProver.expectCollateralBalanceChange(-1 ether); // Lost stake from lock + expectMarketBalanceUnchanged(); + } + + function testFulfillLockedRequestClaimDigestWithFulfillmentDataImageIdAndJournal() public { + Client client = getClient(1); + bytes32 claimDigest = ReceiptClaimLib.ok(APP_IMAGE_ID, sha256(APP_JOURNAL)).digest(); + + // Create request + ProofRequest memory request = client.request(1); + request.requirements.predicate = PredicateLibrary.createClaimDigestMatchPredicate(claimDigest); + + bytes memory clientSignature = client.sign(request); + client.snapshotBalance(); + testProver.snapshotBalance(); + + // Lock and fulfill the request + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress, FulfillmentDataType.ImageIdAndJournal); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + boundlessMarket.fulfill(fills, assessorReceipt); + + // Verify request state and balances + expectRequestFulfilled(fill.id); + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } + + function testFulfillLockedRequesClaimDigestWithFulfillmentDataNone() public { + Client client = getClient(1); + bytes32 claimDigest = ReceiptClaimLib.ok(APP_IMAGE_ID, sha256(APP_JOURNAL)).digest(); + + // Create request + ProofRequest memory request = client.request(1); + request.requirements.predicate = PredicateLibrary.createClaimDigestMatchPredicate(claimDigest); + + bytes memory clientSignature = client.sign(request); + client.snapshotBalance(); + testProver.snapshotBalance(); + + // Lock and fulfill the request + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress, FulfillmentDataType.None); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + boundlessMarket.fulfill(fills, assessorReceipt); + + // Verify request state and balances + expectRequestFulfilled(fill.id); + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } + + // Test that if a callback was requested, but the fulfillment data doesnt have the journal, + // the fulfillment reverts and the callback is not called. + function testFulfillLockedRequestWithCallbackAndFulfillmentDataNone() public { + Client client = getClient(1); + bytes32 claimDigest = ReceiptClaimLib.ok(APP_IMAGE_ID, sha256(APP_JOURNAL)).digest(); + + // Create request with low gas callback + ProofRequest memory request = client.request(1); + request.requirements.callback = Callback({addr: address(mockCallback), gasLimit: 500_000}); + request.requirements.predicate = PredicateLibrary.createClaimDigestMatchPredicate(claimDigest); + + bytes memory clientSignature = client.sign(request); + client.snapshotBalance(); + testProver.snapshotBalance(); + + // Lock and fulfill the request + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress, FulfillmentDataType.None); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectRevert(IBoundlessMarket.UnfulfillableCallback.selector); + boundlessMarket.fulfill(fills, assessorReceipt); + + // Verify callback was not called + assertEq(mockCallback.getCallCount(), 0, "Callback should be called exactly 0 times"); + + // Verify request state and balances + expectRequestNotFulfilled(fill.id); + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(0 ether); + expectMarketBalanceUnchanged(); + } + + function testFulfillLockedRequestClaimDigestWithCallbackImageIdAndJournal() public { + Client client = getClient(1); + bytes32 claimDigest = ReceiptClaimLib.ok(APP_IMAGE_ID, sha256(APP_JOURNAL)).digest(); + // Create request + ProofRequest memory request = client.request(1); + request.requirements.callback = Callback({addr: address(mockCallback), gasLimit: 500_000}); + request.requirements.predicate = PredicateLibrary.createClaimDigestMatchPredicate(claimDigest); + + bytes memory clientSignature = client.sign(request); + client.snapshotBalance(); + testProver.snapshotBalance(); + + // Lock and fulfill the request + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + + (Fulfillment memory fill, AssessorReceipt memory assessorReceipt) = + createFillAndSubmitRoot(request, APP_JOURNAL, testProverAddress, FulfillmentDataType.ImageIdAndJournal); + Fulfillment[] memory fills = new Fulfillment[](1); + fills[0] = fill; + + vm.expectEmit(true, true, true, true); + emit IBoundlessMarket.RequestFulfilled(request.id, testProverAddress, fill.requestDigest); + vm.expectEmit(true, true, true, false); + emit IBoundlessMarket.ProofDelivered(request.id, testProverAddress, fill); + vm.expectEmit(true, true, true, true); + emit MockCallback.MockCallbackCalled(APP_IMAGE_ID, APP_JOURNAL, fill.seal); + + boundlessMarket.fulfill(fills, assessorReceipt); + + assertEq(mockCallback.getCallCount(), 1, "Callback should be called exactly 1 time"); + + // Verify request state and balances + expectRequestFulfilled(fill.id); + client.expectBalanceChange(-1 ether); + testProver.expectBalanceChange(1 ether); + expectMarketBalanceUnchanged(); + } +} + +contract BoundlessMarketLegacyBench is BoundlessMarketLegacyTest { + using BoundlessMarketLib for Offer; + + function benchFulfill(uint256 batchSize, string memory snapshot) public { + (ProofRequest[] memory requests, bytes[] memory journals) = newBatch(batchSize); + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) = + createFillsAndSubmitRoot(requests, journals, testProverAddress); + + boundlessMarket.fulfill(fills, assessorReceipt); + vm.snapshotGasLastCall(string.concat("fulfill: batch of ", snapshot)); + + for (uint256 j = 0; j < fills.length; j++) { + expectRequestFulfilled(fills[j].id); + } + } + + function benchFulfillWithSelector(uint256 batchSize, string memory snapshot) public { + (ProofRequest[] memory requests, bytes[] memory journals) = + newBatchWithSelector(batchSize, setVerifier.SELECTOR()); + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) = + createFillsAndSubmitRoot(requests, journals, testProverAddress); + + boundlessMarket.fulfill(fills, assessorReceipt); + vm.snapshotGasLastCall(string.concat("fulfill (with selector): batch of ", snapshot)); + + for (uint256 j = 0; j < fills.length; j++) { + expectRequestFulfilled(fills[j].id); + } + } + + function benchFulfillWithCallback(uint256 batchSize, string memory snapshot) public { + (ProofRequest[] memory requests, bytes[] memory journals) = newBatchWithCallback(batchSize); + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) = + createFillsAndSubmitRoot(requests, journals, testProverAddress); + + boundlessMarket.fulfill(fills, assessorReceipt); + vm.snapshotGasLastCall(string.concat("fulfill (with callback): batch of ", snapshot)); + + for (uint256 j = 0; j < fills.length; j++) { + expectRequestFulfilled(fills[j].id); + } + } + + function testBenchFulfill001() public { + benchFulfill(1, "001"); + } + + function testBenchFulfill002() public { + benchFulfill(2, "002"); + } + + function testBenchFulfill004() public { + benchFulfill(4, "004"); + } + + function testBenchFulfill008() public { + benchFulfill(8, "008"); + } + + function testBenchFulfill016() public { + benchFulfill(16, "016"); + } + + function testBenchFulfill032() public { + benchFulfill(32, "032"); + } + + function testBenchFulfill064() public { + benchFulfill(64, "064"); + } + + function testBenchFulfill128() public { + benchFulfill(128, "128"); + } + + function testBenchFulfillWithSelector001() public { + benchFulfillWithSelector(1, "001"); + } + + function testBenchFulfillWithSelector002() public { + benchFulfillWithSelector(2, "002"); + } + + function testBenchFulfillWithSelector004() public { + benchFulfillWithSelector(4, "004"); + } + + function testBenchFulfillWithSelector008() public { + benchFulfillWithSelector(8, "008"); + } + + function testBenchFulfillWithSelector016() public { + benchFulfillWithSelector(16, "016"); + } + + function testBenchFulfillWithSelector032() public { + benchFulfillWithSelector(32, "032"); + } + + function testBenchFulfillWithCallback001() public { + benchFulfillWithCallback(1, "001"); + } + + function testBenchFulfillWithCallback002() public { + benchFulfillWithCallback(2, "002"); + } + + function testBenchFulfillWithCallback004() public { + benchFulfillWithCallback(4, "004"); + } + + function testBenchFulfillWithCallback008() public { + benchFulfillWithCallback(8, "008"); + } + + function testBenchFulfillWithCallback016() public { + benchFulfillWithCallback(16, "016"); + } + + function testBenchFulfillWithCallback032() public { + benchFulfillWithCallback(32, "032"); + } +} + +contract BoundlessMarketLegacyUpgradeTest is BoundlessMarketLegacyTest { + using BoundlessMarketLib for Offer; + + function testUnsafeUpgrade() public { + vm.startPrank(ownerWallet.addr); + proxy = UnsafeUpgrades.deployUUPSProxy( + address( + new BoundlessMarket( + setVerifier, + setVerifier, + ASSESSOR_IMAGE_ID, + DEPRECATED_ASSESSOR_IMAGE_ID, + DEPRECATED_ASSESSOR_DURATION, + address(0x01) + ) + ), + abi.encodeCall(BoundlessMarket.initialize, (ownerWallet.addr, "https://assessor.dev.null")) + ); + boundlessMarket = BoundlessMarket(proxy); + address implAddressV1 = UnsafeUpgrades.getImplementationAddress(proxy); + + // Should emit an `Upgraded` event + vm.expectEmit(false, true, true, true); + emit IERC1967.Upgraded(address(0)); + UnsafeUpgrades.upgradeProxy( + proxy, + address( + new BoundlessMarket( + setVerifier, + setVerifier, + ASSESSOR_IMAGE_ID, + DEPRECATED_ASSESSOR_IMAGE_ID, + DEPRECATED_ASSESSOR_DURATION, + address(0x01) + ) + ), + "", + ownerWallet.addr + ); + vm.stopPrank(); + address implAddressV2 = UnsafeUpgrades.getImplementationAddress(proxy); + + assertFalse(implAddressV2 == implAddressV1); + + (bytes32 imageId, string memory imageUrl) = boundlessMarket.imageInfo(); + assertEq(imageId, ASSESSOR_IMAGE_ID, "Image ID should be the same after upgrade"); + assertEq(imageUrl, "https://assessor.dev.null", "Image URL should be the same after upgrade"); + } + + function testGrantAdminRole() public { + address newAdmin = vm.createWallet("NEW_ADMIN").addr; + bytes32 adminRole = boundlessMarket.ADMIN_ROLE(); + + vm.prank(ownerWallet.addr); + boundlessMarket.grantRole(adminRole, newAdmin); + + assertTrue(boundlessMarket.hasRole(adminRole, newAdmin), "New admin should have admin role"); + assertTrue(boundlessMarket.hasRole(adminRole, ownerWallet.addr), "Original owner should still have admin role"); + } +} diff --git a/contracts/test/legacy/BoundlessMarketLegacyViaFallback.t.sol b/contracts/test/legacy/BoundlessMarketLegacyViaFallback.t.sol new file mode 100644 index 000000000..2be808bdf --- /dev/null +++ b/contracts/test/legacy/BoundlessMarketLegacyViaFallback.t.sol @@ -0,0 +1,108 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. + +pragma solidity ^0.8.26; + +import {UnsafeUpgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +import {BoundlessMarket} from "../../src/legacy/BoundlessMarketLegacy.sol"; +import {BoundlessMarket as BoundlessMarketNew} from "../../src/BoundlessMarket.sol"; +import {IBoundlessRouter} from "../../src/router/interfaces/IBoundlessRouter.sol"; +// ASSESSOR_IMAGE_ID and APP_JOURNAL are imported (and thereby re-exported) so that +// CrossABI.t.sol can keep sourcing them from this file, as it did when this file +// declared them itself. +import { + BoundlessMarketLegacyTest, + BoundlessMarketLegacyBasicTest, + BoundlessMarketLegacyBench, + BoundlessMarketLegacyUpgradeTest, + ASSESSOR_IMAGE_ID, + APP_JOURNAL, + DEPRECATED_ASSESSOR_IMAGE_ID, + DEPRECATED_ASSESSOR_DURATION +} from "./BoundlessMarketLegacy.t.sol"; + +/// @dev Re-runs the entire legacy ABI test battery from BoundlessMarketLegacy.t.sol, +/// but against the NEW market deployed in front of the legacy impl. Legacy-only +/// selectors reach the legacy bodies through BoundlessMarket.fallback(), while +/// selectors the new market declares execute on the new impl. Only the deployment +/// (_deployMarket) and the two signature-recovery expectations (which depend on the +/// proxy address) differ from the base suite; every test body is inherited. +/// +/// @dev This base carries only the via-fallback deployment override; it declares no +/// tests of its own. The concrete suites below pick up the legacy test battery, +/// and CrossABI.t.sol extends this base to add cross-ABI-only invariants. +abstract contract BoundlessMarketLegacyViaFallbackTest is BoundlessMarketLegacyTest { + function _deployMarket() internal virtual override { + // Deploy the LEGACY implementation. This is what the fallback delegate- + // calls into for any selector the new market does not declare. + address legacyImpl = address( + new BoundlessMarket( + setVerifier, + setVerifier, + ASSESSOR_IMAGE_ID, + DEPRECATED_ASSESSOR_IMAGE_ID, + DEPRECATED_ASSESSOR_DURATION, + address(collateralToken) + ) + ); + + // Deploy the NEW market impl and point the proxy at it. The new market's + // router is set to a non-zero placeholder address since these tests + // exercise the legacy ABI surface, which is forwarded via fallback + // before the router is ever touched. + boundlessMarketSource = + address(new BoundlessMarketNew(IBoundlessRouter(address(0xdead)), address(collateralToken), legacyImpl)); + proxy = UnsafeUpgrades.deployUUPSProxy( + boundlessMarketSource, abi.encodeCall(BoundlessMarketNew.initialize, (ownerWallet.addr)) + ); + // boundlessMarket is typed as the LEGACY contract so all calls below + // emit the legacy ABI selectors. Selectors that exist on the new + // market (lockRequest, slash, accounts, etc.) execute on the new + // impl; legacy-only selectors (fulfill with the old shape, + // imageInfo, verifyDelivery, etc.) fall through to the legacy impl + // via fallback(). + boundlessMarket = BoundlessMarket(payable(proxy)); + } +} + +contract BoundlessMarketLegacyViaFallbackBasicTest is + BoundlessMarketLegacyBasicTest, + BoundlessMarketLegacyViaFallbackTest +{ + // Disambiguate the diamond: _deployMarket is inherited both from the legacy base + // (BoundlessMarketLegacyBasicTest) and the via-fallback override. Resolve to the + // via-fallback deployment. + function _deployMarket() internal override(BoundlessMarketLegacyTest, BoundlessMarketLegacyViaFallbackTest) { + BoundlessMarketLegacyViaFallbackTest._deployMarket(); + } + + // The recovered addresses differ from the standalone legacy suite by one CREATE + // nonce: _deployMarket here also deploys the new market impl before the proxy, so + // the proxy address (and thus the EIP-712 domain separator) shifts. These are the + // deterministic recoveries against this configuration. + function _expectedIncorrectRequestSigner() internal pure override returns (address) { + return address(0xf9D65aDD060EeC50A7e86C29d91fBEAaC0eDe727); + } + + function _expectedIncorrectDomainSigner() internal pure override returns (address) { + return address(0x27940eD27511Eef63A19320520D3fC30a4F35a56); + } +} + +contract BoundlessMarketLegacyViaFallbackBench is BoundlessMarketLegacyBench, BoundlessMarketLegacyViaFallbackTest { + function _deployMarket() internal override(BoundlessMarketLegacyTest, BoundlessMarketLegacyViaFallbackTest) { + BoundlessMarketLegacyViaFallbackTest._deployMarket(); + } +} + +contract BoundlessMarketLegacyViaFallbackUpgradeTest is + BoundlessMarketLegacyUpgradeTest, + BoundlessMarketLegacyViaFallbackTest +{ + function _deployMarket() internal override(BoundlessMarketLegacyTest, BoundlessMarketLegacyViaFallbackTest) { + BoundlessMarketLegacyViaFallbackTest._deployMarket(); + } +} diff --git a/contracts/test/legacy/CrossABI.t.sol b/contracts/test/legacy/CrossABI.t.sol new file mode 100644 index 000000000..995234bd8 --- /dev/null +++ b/contracts/test/legacy/CrossABI.t.sol @@ -0,0 +1,128 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. + +pragma solidity ^0.8.26; + +import {Vm} from "forge-std/Vm.sol"; +import { + BoundlessMarketLegacyViaFallbackTest, + ASSESSOR_IMAGE_ID, + APP_JOURNAL +} from "./BoundlessMarketLegacyViaFallback.t.sol"; +import {Client} from "./clients/Client.sol"; +import {ProofRequest} from "../../src/legacy/types/ProofRequest.sol"; +import {Fulfillment} from "../../src/legacy/types/Fulfillment.sol"; +import {AssessorReceipt} from "../../src/legacy/types/AssessorReceipt.sol"; +import {RequestId} from "../../src/legacy/types/RequestId.sol"; + +/// @title CrossABITest +/// @notice Pins down behaviors that are unique to the legacy-via-fallback +/// deployment shape — that is, behaviors not exercised by the +/// standalone-legacy or standalone-new test suites in isolation. +/// +/// Coverage rationale: +/// - Shared selectors (lockRequest, slash, withdraw, every view getter that +/// exists on both contracts) always execute on the new market because the +/// dispatcher matches before the fallback fires. Behavior across these +/// selectors is already covered by BoundlessMarketLegacyViaFallback's 133 +/// tests (which pass through the new impl when selectors collide). +/// - Legacy-only selectors (e.g. fulfill(Fulfillment[], AssessorReceipt), +/// imageInfo, verifyDelivery, VERIFIER, ASSESSOR_ID, the legacy +/// submitRootAnd* variants) are routed via fallback to the legacy impl. +/// The via-fallback suite covers many of these but does not isolate the +/// load-bearing invariants. This file calls them out as named tests so +/// regressions surface clearly. +contract CrossABITest is BoundlessMarketLegacyViaFallbackTest { + /// Legacy-only view methods remain callable via the fallback. + /// `imageInfo()` lives only on the legacy impl, so it routes through + /// `fallback() -> delegatecall(LEGACY_IMPL)`. The new market's + /// initialize(address) signature does not accept an image URL, so the + /// proxy's storage slot 2 (imageUrl) is left empty; the assessor image + /// id, however, is read from an immutable baked into the legacy impl's + /// own bytecode and is unaffected by which impl is active. + function testLegacyImageInfoViaFallback() public view { + (bytes32 assessorId, string memory imageUrl) = boundlessMarket.imageInfo(); + assertEq(assessorId, ASSESSOR_IMAGE_ID, "legacy ASSESSOR_ID immutable should round-trip via fallback"); + assertEq(imageUrl, "", "imageUrl slot was not initialized by the new market's initialize signature"); + } + + /// Immutables declared on the legacy impl (here VERIFIER, ASSESSOR_ID) + /// are baked into the legacy impl's bytecode, not the proxy's storage. + /// Reading them through the proxy means executing the legacy impl's + /// auto-generated getter under delegate-call, which pulls the value + /// from the legacy bytecode and returns it. The returned values must + /// match what the legacy impl was constructed with. + function testLegacyImmutablesReadableViaFallback() public { + bytes memory verifierData = _callViaFallback(abi.encodeWithSignature("VERIFIER()")); + bytes memory assessorIdData = _callViaFallback(abi.encodeWithSignature("ASSESSOR_ID()")); + + address verifier = abi.decode(verifierData, (address)); + bytes32 assessorId = abi.decode(assessorIdData, (bytes32)); + + assertEq(verifier, address(setVerifier), "VERIFIER immutable should equal the legacy ctor arg"); + assertEq(assessorId, ASSESSOR_IMAGE_ID, "ASSESSOR_ID immutable should equal the legacy ctor arg"); + } + + /// A selector not declared on either the new market or the legacy impl + /// reverts cleanly. The fallback delegate-calls into the legacy impl; + /// the legacy impl's dispatcher does not match either and returns + /// empty calldata after running out of options, producing a plain + /// revert that propagates back through the fallback. + function testFallbackRevertsOnUnknownSelector() public { + bytes memory data = abi.encodeWithSelector(bytes4(keccak256("nonExistentMethod()"))); + (bool ok,) = address(boundlessMarket).call(data); + assertFalse(ok, "unknown selector must revert"); + } + + /// After a legacy fulfill that flows through fallback, the new market's + /// shared view selectors (requestIsFulfilled lives on both contracts; + /// it routes to the new impl because the selector collides) read the + /// state the legacy impl wrote. This is the load-bearing storage + /// interop invariant for cross-ABI lifecycles: write via legacy, read + /// via new. + function testSharedViewReadsLegacyFulfilledState() public { + Client client = getClient(0); + ProofRequest memory request = client.request(0); + bytes memory clientSignature = client.sign(request); + + // Lock via the shared lockRequest selector (executes on the new market). + vm.prank(testProverAddress); + boundlessMarket.lockRequest(request, clientSignature); + + // Build a single-request fulfillment and submit via the legacy + // fulfill(Fulfillment[], AssessorReceipt) entrypoint. That selector + // is legacy-only — the new market declares fulfill(FulfillmentBatch[]) + // at a different selector — so the call falls through fallback() into + // the legacy impl. + ProofRequest[] memory requests = new ProofRequest[](1); + requests[0] = request; + bytes[] memory journals = new bytes[](1); + journals[0] = APP_JOURNAL; + (Fulfillment[] memory fills, AssessorReceipt memory assessorReceipt) = + createFillsAndSubmitRoot(requests, journals, testProverAddress); + + vm.prank(testProverAddress); + boundlessMarket.fulfill(fills, assessorReceipt); + + // Read state via the shared view: the new market's + // requestIsFulfilled dispatcher entry executes; it queries the same + // accounts/requestLocks slots the legacy impl just wrote to. + assertTrue( + boundlessMarket.requestIsFulfilled(request.id), + "shared requestIsFulfilled must see the state written by the legacy fulfill" + ); + } + + /// @dev Low-level helper to issue a call through the proxy. Used for + /// methods that are not declared on the legacy contract type the + /// test imports (e.g. immutable getters that exist only on the + /// legacy contract symbol, which is not what `boundlessMarket` is + /// cast to at the test's import-level type). + function _callViaFallback(bytes memory data) internal returns (bytes memory) { + (bool ok, bytes memory ret) = address(boundlessMarket).call(data); + assertTrue(ok, "call via fallback should succeed"); + return ret; + } +} diff --git a/contracts/test/legacy/MockCallback.sol b/contracts/test/legacy/MockCallback.sol new file mode 100644 index 000000000..1de8b33d6 --- /dev/null +++ b/contracts/test/legacy/MockCallback.sol @@ -0,0 +1,52 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {IRiscZeroVerifier} from "risc0/IRiscZeroVerifier.sol"; +import {BoundlessMarketCallback} from "../../src/BoundlessMarketCallback.sol"; + +/// @notice Mock callback contract for testing BoundlessMarket callbacks +/// @dev This contract allows configuring how much gas the callback should consume +contract MockCallback is BoundlessMarketCallback { + uint256 public callCount; + uint256 public targetGas; + + event MockCallbackCalled(bytes32 imageId, bytes journal, bytes seal); + + // Store info about each call + struct CallInfo { + bytes32 imageId; + bytes journal; + bytes seal; + } + + // Mapping used for mocking gas consumption + mapping(bytes32 => uint256) private gasConsumptionSlots; + + constructor(IRiscZeroVerifier verifier, address boundlessMarket, bytes32 imageId, uint256 _targetGas) + BoundlessMarketCallback(verifier, boundlessMarket, imageId) + { + targetGas = _targetGas; + } + + function _handleProof(bytes32 imageId, bytes calldata journal, bytes calldata seal) internal override { + uint256 startGas = gasleft(); + + emit MockCallbackCalled(imageId, journal, seal); + callCount++; + + // Consume gas by doing SSTORE operations to random slots + uint256 i = 0; + while (startGas - gasleft() < targetGas) { + bytes32 slot = keccak256(abi.encode(i)); + gasConsumptionSlots[slot] = i; + i++; + } + } + + function getCallCount() external view returns (uint256) { + return callCount; + } +} diff --git a/contracts/test/legacy/TestUtils.sol b/contracts/test/legacy/TestUtils.sol new file mode 100644 index 000000000..8c8312682 --- /dev/null +++ b/contracts/test/legacy/TestUtils.sol @@ -0,0 +1,248 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. + +pragma solidity ^0.8.26; + +import {ReceiptClaim, ReceiptClaimLib} from "risc0/IRiscZeroVerifier.sol"; +import {Seal, RiscZeroSetVerifier} from "risc0/RiscZeroSetVerifier.sol"; +import {Selector} from "../../src/legacy/types/Selector.sol"; +import {AssessorCallback} from "../../src/legacy/types/AssessorCallback.sol"; +import {AssessorCommitment} from "../../src/legacy/types/AssessorCommitment.sol"; +import {AssessorJournal} from "../../src/legacy/types/AssessorJournal.sol"; +import {Fulfillment} from "../../src/legacy/types/Fulfillment.sol"; +import {MerkleProofish} from "../../src/legacy/libraries/MerkleProofish.sol"; + +library TestUtils { + using ReceiptClaimLib for ReceiptClaim; + + bytes8 internal constant LEAF_TAG = bytes8("LEAF_TAG"); + + function mockAssessor( + Fulfillment[] memory fills, + bytes32 assessorImageId, + Selector[] memory selectors, + AssessorCallback[] memory callbacks, + address prover + ) internal pure returns (ReceiptClaim memory) { + bytes32[] memory leaves = new bytes32[](fills.length); + + for (uint256 i = 0; i < fills.length; i++) { + leaves[i] = AssessorCommitment( + i, fills[i].id, fills[i].requestDigest, fills[i].claimDigest, fills[i].fulfillmentDataDigest() + ).eip712Digest(); + } + + bytes32 root = MerkleProofish.processTree(leaves); + + bytes memory journal = + abi.encode(AssessorJournal({root: root, selectors: selectors, callbacks: callbacks, prover: prover})); + return ReceiptClaimLib.ok(assessorImageId, sha256(journal)); + } + + function mockAssessorSeal(RiscZeroSetVerifier setVerifier, bytes32 claimDigest) + internal + view + returns (bytes memory) + { + bytes32[] memory path = new bytes32[](1); + path[0] = claimDigest; + return encodeSeal(setVerifier, Proof({siblings: path})); + } + + function mockSetBuilder(Fulfillment[] memory fills) + internal + pure + returns (bytes32 batchRoot, bytes32[][] memory tree) + { + bytes32[] memory claimDigests = new bytes32[](fills.length); + for (uint256 i = 0; i < fills.length; i++) { + claimDigests[i] = fills[i].claimDigest; + } + // compute the merkle tree of the batch + (batchRoot, tree) = computeMerkleTree(claimDigests); + } + + function fillInclusionProofs( + RiscZeroSetVerifier setVerifier, + Fulfillment[] memory fills, + bytes32 assessorLeaf, + bytes32[][] memory tree + ) internal view { + // generate inclusion proofs for each claim + Proof[] memory proofs = computeProofs(tree); + + for (uint256 i = 0; i < fills.length; i++) { + fills[i].seal = encodeSeal(setVerifier, append(proofs[i], assessorLeaf)); + } + } + + struct Proof { + bytes32[] siblings; + } + + // Build the Merkle Tree and return the root and the entire tree structure + function computeMerkleTree(bytes32[] memory values) internal pure returns (bytes32 root, bytes32[][] memory tree) { + require(values.length > 0, "Values list is empty, cannot compute Merkle root"); + + // Calculate the height of the tree (number of levels) + uint256 numLevels = log2Ceil(values.length) + 1; + + // Initialize the tree structure + tree = new bytes32[][](numLevels); + + // Hash the values with the leaf tag to form the leaf nodes. + tree[0] = new bytes32[](values.length); + for (uint256 i = 0; i < values.length; i++) { + tree[0][i] = hashLeaf(values[i]); + } + + // Build the tree level by level + uint256 currentLevelSize = values.length; + for (uint256 level = 0; currentLevelSize > 1; level++) { + uint256 nextLevelSize = (currentLevelSize + 1) / 2; + tree[level + 1] = new bytes32[](nextLevelSize); + + for (uint256 i = 0; i < nextLevelSize; i++) { + uint256 leftIndex = i * 2; + uint256 rightIndex = leftIndex + 1; + + bytes32 leftHash = tree[level][leftIndex]; + if (rightIndex < currentLevelSize) { + bytes32 rightHash = tree[level][rightIndex]; + + tree[level + 1][i] = MerkleProofish._hashPair(leftHash, rightHash); + } else { + // If the node has no right sibling, copy it up to the next level. + tree[level + 1][i] = leftHash; + } + } + + currentLevelSize = nextLevelSize; + } + + root = tree[tree.length - 1][0]; + } + + function computeProofs(bytes32[][] memory tree) internal pure returns (Proof[] memory proofs) { + uint256 numLeaves = tree[0].length; + uint256 proofLength = tree.length - 1; // Maximum possible length of the proof + proofs = new Proof[](numLeaves); + + // Generate proof for each leaf + for (uint256 leafIndex = 0; leafIndex < numLeaves; leafIndex++) { + bytes32[] memory tempSiblings = new bytes32[](proofLength); + uint256 actualProofLength = 0; + uint256 index = leafIndex; + + // Collect the siblings for the proof + for (uint256 level = 0; level < tree.length - 1; level++) { + uint256 siblingIndex = (index % 2 == 0) ? index + 1 : index - 1; + + if (siblingIndex < tree[level].length) { + tempSiblings[actualProofLength] = tree[level][siblingIndex]; + actualProofLength++; + } + + index /= 2; + } + + // Adjust the length of the proof to exclude any unused slots + proofs[leafIndex].siblings = new bytes32[](actualProofLength); + for (uint256 i = 0; i < actualProofLength; i++) { + proofs[leafIndex].siblings[i] = tempSiblings[i]; + } + } + } + + function hashLeaf(bytes32 value) internal pure returns (bytes32 leaf) { + return keccak256(abi.encodePacked(LEAF_TAG, value)); + } + + function encodeSeal(RiscZeroSetVerifier setVerifier, TestUtils.Proof memory merkleProof, bytes memory rootSeal) + internal + view + returns (bytes memory) + { + return abi.encodeWithSelector(setVerifier.SELECTOR(), Seal({path: merkleProof.siblings, rootSeal: rootSeal})); + } + + function encodeSeal(RiscZeroSetVerifier setVerifier, TestUtils.Proof memory merkleProof) + internal + view + returns (bytes memory) + { + bytes memory rootSeal; + return encodeSeal(setVerifier, merkleProof, rootSeal); + } + + function append(Proof memory proof, bytes32 newNode) internal pure returns (Proof memory) { + bytes32[] memory newSiblings = new bytes32[](proof.siblings.length + 1); + for (uint256 i = 0; i < proof.siblings.length; i++) { + newSiblings[i] = proof.siblings[i]; + } + newSiblings[proof.siblings.length] = newNode; + proof.siblings = newSiblings; + return proof; + } + + function log2Ceil(uint256 x) private pure returns (uint256) { + uint256 res = 0; + uint256 value = x; + while (value > 1) { + value = (value + 1) / 2; + res += 1; + } + return res; + } + + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + + // computes the hash of a permit + function getPermitHash(address owner, address spender, uint256 value, uint256 nonce, uint256 deadline) + public + pure + returns (bytes32) + { + return keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline)); + } + + /// @notice Adds a non-zero selector at the given index + /// @dev Overwrites any existing selector at that index + /// @param self The Selectors struct to modify + /// @param index The index where to add the selector + /// @param selector The selector to add + function addSelector(Selector[] memory self, uint8 index, bytes4 selector) + internal + pure + returns (Selector[] memory) + { + // Create a new array with one additional element. + Selector[] memory newSelectors = new Selector[](self.length + 1); + for (uint256 i = 0; i < self.length; i++) { + newSelectors[i] = self[i]; + } + newSelectors[self.length] = Selector(index, selector); + return newSelectors; + } + + /// @notice Adds a non-zero callback at the given index + /// @dev Overwrites any existing callback at that index + /// @param self The Callbacks struct to modify + /// @param callback The callback to add + function addCallback(AssessorCallback[] memory self, AssessorCallback memory callback) + internal + pure + returns (AssessorCallback[] memory) + { + // Create a new array with one additional element. + AssessorCallback[] memory newCallbacks = new AssessorCallback[](self.length + 1); + for (uint256 i = 0; i < self.length; i++) { + newCallbacks[i] = self[i]; + } + newCallbacks[self.length] = callback; + return newCallbacks; + } +} diff --git a/contracts/test/legacy/clients/BaseClient.sol b/contracts/test/legacy/clients/BaseClient.sol new file mode 100644 index 000000000..3ccab695a --- /dev/null +++ b/contracts/test/legacy/clients/BaseClient.sol @@ -0,0 +1,110 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {IBoundlessMarket} from "../../../src/legacy/IBoundlessMarketLegacy.sol"; +import {HitPoints} from "../../../src/HitPoints.sol"; +import {Vm} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {Callback} from "../../../src/legacy/types/Callback.sol"; +import {ProofRequest} from "../../../src/legacy/types/ProofRequest.sol"; +import {LockRequest} from "../../../src/legacy/types/LockRequest.sol"; +import {Offer} from "../../../src/legacy/types/Offer.sol"; +import {Requirements} from "../../../src/legacy/types/Requirements.sol"; +import {PredicateLibrary} from "../../../src/legacy/types/Predicate.sol"; + +import {IBoundlessMarket} from "../../../src/legacy/IBoundlessMarketLegacy.sol"; + +Vm constant VM = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); +bytes32 constant APP_IMAGE_ID = 0x0000000000000000000000000000000000000000000000000000000000000001; +bytes32 constant SET_BUILDER_IMAGE_ID = 0x0000000000000000000000000000000000000000000000000000000000000002; +bytes32 constant ASSESSOR_IMAGE_ID = 0x0000000000000000000000000000000000000000000000000000000000000003; +bytes constant APP_JOURNAL = bytes("GUEST JOURNAL"); + +abstract contract BaseClient { + using SafeCast for uint256; + using SafeCast for int256; + + int256 public balanceSnapshot = type(int256).max; + int256 public stakeBalanceSnapshot = type(int256).max; + + string public identifier; + + IBoundlessMarket public boundlessMarket; + HitPoints public collateralToken; + + constructor() {} + + function initialize(string memory _identifier, IBoundlessMarket _boundlessMarket, HitPoints _collateralToken) + public + virtual + { + identifier = _identifier; + boundlessMarket = _boundlessMarket; + collateralToken = _collateralToken; + balanceSnapshot = type(int256).max; + } + + function addr() public view virtual returns (address); + + function sign(ProofRequest calldata req) public virtual returns (bytes memory); + + function signLockRequest(LockRequest calldata req) public virtual returns (bytes memory); + + function defaultOffer() public view returns (Offer memory) { + return Offer({ + minPrice: 1 ether, + maxPrice: 2 ether, + rampUpStart: uint64(block.timestamp), + rampUpPeriod: uint32(10), + lockTimeout: uint32(100), + timeout: uint32(200), + lockCollateral: 1 ether + }); + } + + function defaultRequirements() public pure returns (Requirements memory) { + return Requirements({ + predicate: PredicateLibrary.createDigestMatchPredicate(bytes32(APP_IMAGE_ID), sha256(APP_JOURNAL)), + selector: bytes4(0), + callback: Callback({addr: address(0), gasLimit: 0}) + }); + } + + function request(uint32 idx) public virtual returns (ProofRequest memory); + + function request(uint32 idx, Offer memory offer) public virtual returns (ProofRequest memory); + + function snapshotBalance() public { + balanceSnapshot = boundlessMarket.balanceOf(addr()).toInt256(); + } + + function snapshotCollateralBalance() public { + stakeBalanceSnapshot = boundlessMarket.balanceOfCollateral(addr()).toInt256(); + } + + function expectBalanceChange(int256 change) public view { + require(balanceSnapshot != type(int256).max, "balance snapshot is not set"); + int256 newBalance = boundlessMarket.balanceOf(addr()).toInt256(); + console.log("%s balance at block %d: %d", identifier, block.number, newBalance.toUint256()); + int256 expectedBalance = balanceSnapshot + change; + require(expectedBalance >= 0, "expected balance cannot be less than 0"); + console.log("%s expected balance at block %d: %d", identifier, block.number, expectedBalance.toUint256()); + require(expectedBalance == newBalance, "balance is not equal to expected value"); + } + + function expectCollateralBalanceChange(int256 change) public view { + require(stakeBalanceSnapshot != type(int256).max, "collateral balance snapshot is not set"); + int256 newBalance = boundlessMarket.balanceOfCollateral(addr()).toInt256(); + console.log("%s collateral balance at block %d: %d", identifier, block.number, newBalance.toUint256()); + int256 expectedBalance = stakeBalanceSnapshot + change; + require(expectedBalance >= 0, "expected collateral balance cannot be less than 0"); + console.log( + "%s expected collateral balance at block %d: %d", identifier, block.number, expectedBalance.toUint256() + ); + require(expectedBalance == newBalance, "collateral balance is not equal to expected value"); + } +} diff --git a/contracts/test/legacy/clients/Client.sol b/contracts/test/legacy/clients/Client.sol new file mode 100644 index 000000000..0f5e9532e --- /dev/null +++ b/contracts/test/legacy/clients/Client.sol @@ -0,0 +1,81 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {BaseClient} from "./BaseClient.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; +import {TestUtils} from "../TestUtils.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {ProofRequest} from "../../../src/legacy/types/ProofRequest.sol"; +import {RequestIdLibrary} from "../../../src/legacy/types/RequestId.sol"; +import {Input, InputType} from "../../../src/legacy/types/Input.sol"; +import {Offer} from "../../../src/legacy/types/Offer.sol"; +import {LockRequest} from "../../../src/legacy/types/LockRequest.sol"; + +Vm constant VM = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +/// @dev Client is a wrapper around an EOA with logic for signing proof requests and submitting them to the market. +/// It also inherits functions for tracking balances and stake from BaseClient. +contract Client is BaseClient { + Vm.Wallet public wallet; + + constructor(Vm.Wallet memory _wallet) { + wallet = _wallet; + } + + function addr() public view override returns (address) { + return wallet.addr; + } + + function sign(ProofRequest calldata req) public override returns (bytes memory) { + bytes32 structDigest = + MessageHashUtils.toTypedDataHash(boundlessMarket.eip712DomainSeparator(), req.eip712Digest()); + (uint8 v, bytes32 r, bytes32 s) = VM.sign(wallet, structDigest); + return abi.encodePacked(r, s, v); + } + + function signLockRequest(LockRequest calldata req) public override returns (bytes memory) { + bytes32 structDigest = + MessageHashUtils.toTypedDataHash(boundlessMarket.eip712DomainSeparator(), req.eip712Digest()); + (uint8 v, bytes32 r, bytes32 s) = VM.sign(wallet, structDigest); + return abi.encodePacked(r, s, v); + } + + function request(uint32 idx) public view override returns (ProofRequest memory) { + return ProofRequest({ + id: RequestIdLibrary.from(addr(), idx), + requirements: defaultRequirements(), + imageUrl: "https://image.dev.null", + input: Input({inputType: InputType.Url, data: bytes("https://input.dev.null")}), + offer: defaultOffer() + }); + } + + function request(uint32 idx, Offer memory offer) public view override returns (ProofRequest memory) { + return ProofRequest({ + id: RequestIdLibrary.from(addr(), idx), + requirements: defaultRequirements(), + imageUrl: "https://image.dev.null", + input: Input({inputType: InputType.Url, data: bytes("https://input.dev.null")}), + offer: offer + }); + } + + function signPermit(address spender, uint256 value, uint256 deadline) + public + returns (uint8 v, bytes32 r, bytes32 s) + { + return VM.sign( + wallet, + MessageHashUtils.toTypedDataHash( + collateralToken.DOMAIN_SEPARATOR(), + TestUtils.getPermitHash( + wallet.addr, spender, value, ERC20Permit(address(collateralToken)).nonces(wallet.addr), deadline + ) + ) + ); + } +} diff --git a/contracts/test/legacy/clients/MockSmartContractWallet.sol b/contracts/test/legacy/clients/MockSmartContractWallet.sol new file mode 100644 index 000000000..4660e4d16 --- /dev/null +++ b/contracts/test/legacy/clients/MockSmartContractWallet.sol @@ -0,0 +1,59 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {IBoundlessMarket} from "../../../src/legacy/IBoundlessMarketLegacy.sol"; + +/// @dev Simple mock implementation of an ERC-1271 compliant SCW. +contract MockSmartContractWallet is IERC1271 { + bytes private expectedSignature; + uint256 private gasCost = 0; + address private owner; + IBoundlessMarket public immutable MARKET; + bytes4 internal constant MAGICVALUE = 0x1626ba7e; // bytes4(keccak256("isValidSignature(bytes32,bytes)") + + constructor(bytes memory _expectedSignature, IBoundlessMarket _market, address _owner) { + expectedSignature = _expectedSignature; + MARKET = _market; + owner = _owner; + } + + function setExpectedSignature(bytes memory _expectedSignature) external { + expectedSignature = _expectedSignature; + } + + function setGasCost(uint256 _gasCost) external { + gasCost = _gasCost; + } + + function isValidSignature(bytes32, bytes memory _signature) external view returns (bytes4) { + // Consume gas by doing SLOAD operations to random slots + uint256 startGas = gasleft(); + uint256 i = 0; + while (startGas - gasleft() < gasCost) { + bytes32 slot = keccak256(abi.encode(i)); + bytes32 x; + assembly { + x := sload(slot) + } + i++; + } + + if (keccak256(_signature) == keccak256(expectedSignature)) { + return MAGICVALUE; + } + return 0xffffffff; + } + + // Allow the wallet to receive ETH + receive() external payable {} + + function execute(address target, bytes memory data, uint256 value) external payable { + require(msg.sender == owner, "Not authorized"); + (bool success,) = target.call{value: value}(data); + require(success, "Call failed"); + } +} diff --git a/contracts/test/legacy/clients/SmartContractClient.sol b/contracts/test/legacy/clients/SmartContractClient.sol new file mode 100644 index 000000000..8340cf7d2 --- /dev/null +++ b/contracts/test/legacy/clients/SmartContractClient.sol @@ -0,0 +1,92 @@ +// Copyright 2026 Boundless Foundation, Inc. +// +// Use of this source code is governed by the Business Source License +// as found in the LICENSE-BSL file. +pragma solidity ^0.8.26; + +import {IBoundlessMarket} from "../../../src/legacy/IBoundlessMarketLegacy.sol"; +import {HitPoints} from "../../../src/HitPoints.sol"; +import {BaseClient} from "./BaseClient.sol"; +import {Test} from "forge-std/Test.sol"; +import {MockSmartContractWallet} from "./MockSmartContractWallet.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {ProofRequest} from "../../../src/legacy/types/ProofRequest.sol"; +import {LockRequest} from "../../../src/legacy/types/LockRequest.sol"; +import {RequestIdLibrary} from "../../../src/legacy/types/RequestId.sol"; +import {Input, InputType} from "../../../src/legacy/types/Input.sol"; +import {Offer} from "../../../src/legacy/types/Offer.sol"; + +Vm constant VM = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + +/// @dev SmartContractClient is essentially a wrapper around a smart contract wallet with logic for signing proof requests and submitting them to the market. +/// It also inherits functions for tracking balances and stake from BaseClient. +contract SmartContractClient is BaseClient, Test { + MockSmartContractWallet public smartWallet; + Vm.Wallet public signer; + + bytes private expectedSignature; + + constructor(Vm.Wallet memory _signer) { + expectedSignature = abi.encodePacked(keccak256(abi.encodePacked(_signer.addr))); + smartWallet = new MockSmartContractWallet(expectedSignature, boundlessMarket, _signer.addr); + signer = _signer; + } + + function initialize(string memory _identifier, IBoundlessMarket _boundlessMarket, HitPoints _collateralToken) + public + override + { + vm.label(address(smartWallet), _identifier); + super.initialize(_identifier, _boundlessMarket, _collateralToken); + } + + function addr() public view override returns (address) { + return address(smartWallet); + } + + function signerAddr() public view returns (address) { + return signer.addr; + } + + function request(uint32 idx) public view override returns (ProofRequest memory) { + return ProofRequest({ + id: RequestIdLibrary.from(addr(), idx, true), + requirements: defaultRequirements(), + imageUrl: "https://image.dev.null", + input: Input({inputType: InputType.Url, data: bytes("https://input.dev.null")}), + offer: defaultOffer() + }); + } + + function request(uint32 idx, Offer memory offer) public view override returns (ProofRequest memory) { + return ProofRequest({ + id: RequestIdLibrary.from(addr(), idx, true), + requirements: defaultRequirements(), + imageUrl: "https://image.dev.null", + input: Input({inputType: InputType.Url, data: bytes("https://input.dev.null")}), + offer: offer + }); + } + + function sign(ProofRequest calldata) public view override returns (bytes memory) { + return expectedSignature; + } + + function signLockRequest(LockRequest calldata) public view override returns (bytes memory) { + return expectedSignature; + } + + function execute(address target, bytes memory data) public { + vm.prank(signer.addr); + smartWallet.execute(target, data, 0); + } + + function execute(address target, bytes memory data, uint256 value) public { + vm.prank(signer.addr); + smartWallet.execute(target, data, value); + } + + function setExpectedSignature(bytes memory _expectedSignature) public { + smartWallet.setExpectedSignature(_expectedSignature); + } +} diff --git a/contracts/test/legacy/deployed-bytecode.hex b/contracts/test/legacy/deployed-bytecode.hex new file mode 100644 index 000000000..fcb901a11 --- /dev/null +++ b/contracts/test/legacy/deployed-bytecode.hex @@ -0,0 +1 @@ +0x6080806040526004361015610012575f80fd5b5f905f3560e01c90816301ffc9a7146122125750806308c84e70146121ce5780630b7ae1a71461214157806315d7a240146121265780631ce0302414612108578063248a9ca3146120e95780632abff1f214611fde5780632e107a9014611f5c5780632e1a7d4d14611f3e5780632f2ff15d14611f0c57806336568abe14611ec757806341451f9414611e1657806341d3ab6914611dfb578063444161da14611dc057806345bc4d1014611a525780634cefb7cf14611a2b5780634f1ef2861461184657806352d1902d146117df578063553c0248146117c35780635b07fdd8146117a05780635d704b33146116ef57806360dfd4a9146116575780636112fe2e146114f6578063612bee0c146114d557806370a08231146114925780637136a7f31461147a57806375b238fc146112185780637870d4811461145957806381bf6c241461141057806384b0196e146112e857806391d1485414611292578063956b0960146112755780639f04f420146112585780639fe9428c1461121d578063a217fddf14611218578063ad2fa6c814611190578063ad3cb1cc14611147578063ae7330f1146110a9578063afe171fd14611065578063b09c980b1461101f578063b760faf914610f99578063bad4a01f14610f7a578063c515c15f14610ef5578063c64067a214610edd578063cb74db1114610eb4578063cdc9712314610dbe578063d0e30db014610daa578063d4bd257b14610d0d578063d547741f14610cd2578063df2e670614610c60578063eba2ecc814610c22578063ece510a514610bdd578063ef1ae1c814610b98578063f2800f1a14610b41578063f399e22e14610576578063fd737ea8146104bd578063ff1214a5146102ba5763ffa1ad741461029c575f80fd5b346102b757806003193601126102b757602060405160018152f35b80fd5b50346102b75760603660031901126102b7576004356001600160401b0381116104b957610160816004019160031990360301126104b9576024356001600160401b0381116104b5576103109036906004016122ba565b916044356001600160401b0381116104b1576103309036906004016122ba565b61033a83356143eb565b9161034787878488614735565b6040519195916103586060826125d1565b60218152602081017f4c6f636b526571756573742850726f6f66526571756573742072657175657374815260408201602960f81b90526103966152af565b9061039f6152f9565b8d6103a861533e565b6103b06153fc565b6103b8615449565b916103c16154d0565b94604051978897602089019a5180918c5e880160208101918783528051926020849201905e0160200185815281516020819301825e0184815281516020819301825e0183815281516020819301825e0182815281516020819301825e0190815281516020819301825e018d815203601f198101825261044090826125d1565b5190209060405190602082019283526040820152604081526104636060826125d1565b51902061046e615a7e565b9061047891615b33565b9136906104849261260d565b61048d91615b50565b61049991959295615b8a565b6104a285614d43565b966104ae989196614ee3565b80f35b8480fd5b8280fd5b5080fd5b50346102b75760c03660031901126102b7576104d7612290565b6024358260643560ff811681036104b9577f000000000000000000000000aa61bb7777bd01b684347961918f1e07fbbce7cf6001600160a01b0316803b156104b55760405163d505accf60e01b8152918391839182908490829061054c9060a43590608435906044358d303360048901612d44565b03925af1610561575b50506104ae9133614513565b8161056b916125d1565b6104b557825f610555565b50346102b75760403660031901126102b757610590612290565b906024356001600160401b0381116104b9576105b09036906004016122ba565b5f80516020615ec7833981519152939193549060ff8260401c1615916001600160401b03811680159081610b39575b6001149081610b2f575b159081610b26575b50610b175767ffffffffffffffff1981166001175f80516020615ec78339815191525582610aeb575b506001600160a01b03831615610adc57610632615b08565b61063a615b08565b604092835161064985826125d1565b601081526f12509bdd5b991b195cdcd3585c9ad95d60821b602082015284519061067386836125d1565b60018252603160f81b6020830152610689615b08565b610691615b08565b8051906001600160401b038211610ac8576106b95f80516020615e0783398151915254612825565b601f8111610a59575b50602090601f83116001146109dd576106f292918991836108cf575b50508160011b915f199060031b1c19161790565b5f80516020615e07833981519152555b8051906001600160401b0382116109c95761072a5f80516020615e2783398151915254612825565b601f811161095a575b50602090601f83116001146108da5791806107679261079c95948a926108cf5750508160011b915f199060031b1c19161790565b5f80516020615e27833981519152555b855f80516020615e4783398151915255855f80516020615ee783398151915255613e79565b506001600160401b0381116108bb576107bf816107ba600254612825565b61285d565b83601f821160011461084c57819085966107ee949596926108415750508160011b915f199060031b1c19161790565b6002555b6107fa575080f35b5f80516020615ec7833981519152805460ff60401b1916905551600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290602090a180f35b013590505f806106de565b60028552601f198216955f80516020615de783398151915291865b8881106108a35750836001959697981061088a575b505050811b016002556107f2565b01355f19600384901b60f8161c191690555f808061087c565b90926020600181928686013581550194019101610867565b634e487b7160e01b84526041600452602484fd5b015190505f806106de565b5f80516020615e2783398151915288528188209190601f198416895b818110610942575091600193918561079c9796941061092a575b505050811b015f80516020615e2783398151915255610777565b01515f1960f88460031b161c191690555f8080610910565b929360206001819287860151815501950193016108f6565b5f80516020615e2783398151915288527f5f9ce34815f8e11431c7bb75a8e6886a91478f7ffc1dbb0a98dc240fddd76b75601f840160051c810191602085106109bf575b601f0160051c01905b8181106109b45750610733565b8881556001016109a7565b909150819061099e565b634e487b7160e01b87526041600452602487fd5b5f80516020615e0783398151915289528189209190601f1984168a5b818110610a415750908460019594939210610a29575b505050811b015f80516020615e0783398151915255610702565b01515f1960f88460031b161c191690555f8080610a0f565b929360206001819287860151815501950193016109f9565b5f80516020615e0783398151915289527f42ad5d3e1f2e6e70edcf6d991b8a3023d3fca8047a131592f9edb9fd9b89d57d601f840160051c81019160208510610abe575b601f0160051c01905b818110610ab357506106c2565b898155600101610aa6565b9091508190610a9d565b634e487b7160e01b88526041600452602488fd5b63267eaa8160e21b8452600484fd5b68ffffffffffffffffff191668010000000000000001175f80516020615ec7833981519152555f61061a565b63f92ee8a960e01b8552600485fd5b9050155f6105f1565b303b1591506105e9565b8491506105df565b50346102b75760203660031901126102b75760043590610b60826138db565b15610b86576040816020936001600160401b039352808452205460a01c16604051908152f35b60249163d2be005d60e01b8252600452fd5b50346102b757806003193601126102b7576040517f000000000000000000000000aa61bb7777bd01b684347961918f1e07fbbce7cf6001600160a01b03168152602090f35b50346102b757806003193601126102b7576040517f000000000000000000000000a326b2eb45a5c3c206df905a58970dca57b8719e6001600160a01b03168152602090f35b50346102b7576104ae610c3436612714565b91610c3f81356143eb565b90610c4c85858386614735565b50610c5684614d43565b9690953395614ee3565b507fc354af001adff0e8c35481c5ce3df3edee370c71572514d281e884c8cb552203610c8b36612714565b9291909234610cc5575b610cbf60405192839260408452610caf6040850183613b3c565b9184830360208601523596612767565b0390a280f35b610ccd613a82565b610c95565b50346102b75760403660031901126102b757610d09600435610cf261227a565b90610d04610cff82612807565b613e33565b613fa6565b5080f35b50346102b757610d1c366124c2565b969095919490936001600160a01b039092169190823b156104b15791610d5d939185809460405196879586948593636691f64760e01b855260048501612787565b03925af18015610d9f57610d8a575b610d86610d7a8686866127b2565b6040519182918261240e565b0390f35b610d958280926125d1565b6102b75780610d6c565b6040513d84823e3d90fd5b50806003193601126102b7576104ae613a82565b50346102b757806003193601126102b757604051908060025490610de182612825565b8085529160018116908115610e8d5750600114610e43575b610d8684610e09818603826125d1565b6040519182917f6c5a03c0785e91bc0ad0db486004116010680a03af4e712bcca3188e5669410083526040602084015260408301906123ea565b600281525f80516020615de7833981519152939250905b808210610e7357509091508101602001610e0982610df9565b919260018160209254838588010152019101909291610e5a565b60ff191660208087019190915292151560051b85019092019250610e099150839050610df9565b50346102b75760203660031901126102b7576020610ed36004356138db565b6040519015158152f35b50346102b7576104ae610eef36612714565b91613841565b50346102b75760203660031901126102b757604060e091600435815280602052208054906001600160601b0360026001830154920154916040519360018060a01b03811685526001600160401b038160a01c16602086015262ffffff81871c16604086015260f81c6060850152818116608085015260601c1660a083015260c0820152f35b50346102b75760203660031901126102b7576104ae6004353333614513565b5060203660031901126102b757610fae612290565b610fb7346144e2565b9060018060a01b03169081835260016020526001600160601b03610fe2604085209282845416612cd9565b166001600160601b03198254161790557fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c6020604051348152a280f35b50346102b75760203660031901126102b7576020906001600160601b03906040906001600160a01b03611050612290565b16815260018452205460601c16604051908152f35b50346102b757806003193601126102b75760206040516001600160401b037f0000000000000000000000000000000000000000000000000000000069cf97ef168152f35b50346102b75760603660031901126102b757806110c4612290565b6044356001600160401b038111611143576110e39036906004016122ba565b6001600160a01b0390921691823b1561113e5761111c92849283604051809681958294636691f64760e01b845260243560048501612787565b03925af18015610d9f5761112d5750f35b81611137916125d1565b6102b75780f35b505050fd5b5050fd5b50346102b757806003193601126102b75750610d8660405161116a6040826125d1565b60058152640352e302e360dc1b60208201526040519182916020835260208301906123ea565b50346102b75761119f36612317565b9a93969297909960018060a09b949b9897981b031691823b156104b157916111e2939185809460405196879586948593636691f64760e01b855260048501612787565b03925af18015610d9f57611203575b610d86610d7a8a8a8a8a8a8a8a61376a565b61120e8280926125d1565b6102b757806111f1565b6126fa565b50346102b757806003193601126102b75760206040517f6c5a03c0785e91bc0ad0db486004116010680a03af4e712bcca3188e566941008152f35b50346102b757806003193601126102b757602060405161c3508152f35b50346102b757806003193601126102b75760206040516113888152f35b50346102b75760403660031901126102b75760406112ae61227a565b9160043581525f80516020615ea7833981519152602052209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b50346102b757806003193601126102b7575f80516020615e478339815191525415806113fa575b156113bd5761136190611320613908565b906113296139d5565b90602061136f6040519361133d83866125d1565b8385525f368137604051968796600f60f81b885260e08589015260e08801906123ea565b9086820360408801526123ea565b904660608601523060808601528260a086015284820360c08601528080855193848152019401925b8281106113a657505050500390f35b835185528695509381019392810192600101611397565b60405162461bcd60e51b81526020600482015260156024820152741152540dcc4c8e88155b9a5b9a5d1a585b1a5e9959605a1b6044820152606490fd5b505f80516020615ee7833981519152541561130f565b50346102b75760203660031901126102b75761144d60209160406114356004356143eb565b6001600160a01b039091168352600185529120614434565b90506040519015158152f35b50346102b757610d86610d7a61146e36612661565b9594909493919361376a565b50346102b7576104ae61148c3661246d565b91612eb2565b50346102b75760203660031901126102b7576020906001600160601b03906040906001600160a01b036114c3612290565b16815260018452205416604051908152f35b50346102b757610d86610d7a6114ea36612661565b95949094939193612dbf565b50346102b75760203660031901126102b75760043533825260016020526001600160601b03604083205460601c166001600160601b03611535836144e2565b16116116445761156b611547826144e2565b33845260016020526001600160601b03604085209181835460601c16031690612cf9565b60405163a9059cbb60e01b815233600482015260248101829052602081604481867f000000000000000000000000aa61bb7777bd01b684347961918f1e07fbbce7cf6001600160a01b03165af190811561163957839161160a575b50156115fb576040519081527fa315121c7f539fd811176ad2735d5d3981237b261889ec13ae4d617ad06e39bc60203392a280f35b6312171d8360e31b8252600482fd5b61162c915060203d602011611632575b61162481836125d1565b810190612d2c565b5f6115c6565b503d61161a565b6040513d85823e3d90fd5b63112fed8b60e31b825233600452602482fd5b50346102b75760203660031901126102b757600460606040602093833581528085522060026040519161168983612551565b805460018060a01b03811684526001600160401b038160a01c168785015262ffffff8160e01c16604085015260f81c848401526001600160601b0360018201548181166080860152851c1660a0840152015460c082015201511615156040519015158152f35b50346102b75760a03660031901126102b7576004358160443560ff811681036104b9577f000000000000000000000000aa61bb7777bd01b684347961918f1e07fbbce7cf6001600160a01b0316803b156104b55760405163d505accf60e01b815291839183918290849082906117769060843590606435906024358d303360048901612d44565b03925af161178b575b506104ae823333614513565b81611795916125d1565b6104b957815f61177f565b50346102b757806003193601126102b75760206117bb615a7e565b604051908152f35b50346102b757806003193601126102b757602090604051908152f35b50346102b757806003193601126102b7577f00000000000000000000000022bb6bbe5d221ef3e738029dab4d1d27ec725cd36001600160a01b031630036118375760206040515f80516020615e878339815191528152f35b63703e46dd60e11b8152600490fd5b5060403660031901126102b75761185b612290565b906024356001600160401b0381116104b95761187b903690600401612643565b6001600160a01b037f00000000000000000000000022bb6bbe5d221ef3e738029dab4d1d27ec725cd316308114908115611a09575b506119fa576118bd613df7565b6040516352d1902d60e01b8152926001600160a01b0381169190602085600481865afa809585966119c6575b5061190257634c9c8ce360e01b84526004839052602484fd5b9091845f80516020615e8783398151915281036119b45750813b156119a2575f80516020615e8783398151915280546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b8480a281518390156119885780836020610d0995519101845af4611982613cf6565b91615d88565b505050346119935780f35b63b398979f60e01b8152600490fd5b634c9c8ce360e01b8452600452602483fd5b632a87526960e21b8552600452602484fd5b9095506020813d6020116119f2575b816119e2602093836125d1565b810103126104b15751945f6118e9565b3d91506119d5565b63703e46dd60e11b8252600482fd5b5f80516020615e87833981519152546001600160a01b0316141590505f6118b0565b50346102b75760403660031901126102b7576104ae611a48612290565b6024359033614513565b50346102b75760203660031901126102b757600435611a93611a73826143eb565b6001600160a01b0390911680855260016020526040852090929190614434565b5015611dac57818352826020526040832060405190611ab182612551565b805460018060a01b03811683526001600160401b038160a01c16602084015262ffffff8160e01c16604084015260f81c60608301526001810154600260808401926001600160601b03831684526001600160601b0360a086019360601c168352015460c08401526004606084015116611d98576001606084015116611d84576001600160401b03611b4184614062565b16421115611d5b5784865260208690526040862080546001600160f81b03811660f891821c60041790911b6001600160f81b0319161781558690600101556001600160601b038151166113888102908082046113881490151715611d4757611bbe6001600160601b039392612710611bc3930494859151166129e2565b6144e2565b936002606060018060a01b038651169501511615155f14611ce357505060018060a01b03821685526001602052611c1460408620611c0e856001600160601b03835460601c16612cd9565b90612cf9565b60405163a9059cbb60e01b815261dead60048201526024810182905291602083604481897f000000000000000000000000aa61bb7777bd01b684347961918f1e07fbbce7cf6001600160a01b03165af18015611cd8577f79ca7c80cf57b513ffdf8aa37ec70e40757f5e0d35219241860bb4b4c2fa7616946060946001600160601b0392611cbb575b5060405193845216602083015260018060a01b03166040820152a280f35b611cd39060203d6020116116325761162481836125d1565b611c9d565b6040513d88823e3d90fd5b9092506001600160601b0330933088526001602052611d0f60408920611c0e8885835460601c16612cd9565b511690865260016020526001600160601b03611d32604088209282845416612cd9565b166001600160601b0319825416179055611c14565b634e487b7160e01b87526011600452602487fd5b6044866001600160401b0387611d7087614062565b9063079c66ab60e41b845260045216602452fd5b631cfdeebb60e01b86526004859052602486fd5b633231064d60e11b86526004859052602486fd5b63d2be005d60e01b83526004829052602483fd5b50346102b757806003193601126102b75760206040517f6c5a03c0785e91bc0ad0db486004116010680a03af4e712bcca3188e566941008152f35b50346102b757610d86610d7a611e103661246d565b916129ef565b50346102b75760203660031901126102b75760043590611e35826138db565b15610b8657604081602093611eb6935280845220600260405191611e5883612551565b805460018060a01b03811684526001600160401b038160a01c168685015262ffffff8160e01c16604085015260f81c60608401526001600160601b036001820154818116608086015260601c1660a0840152015460c0820152614062565b6001600160401b0360405191168152f35b50346102b75760403660031901126102b757611ee161227a565b336001600160a01b03821603611efd57610d0990600435613fa6565b63334bd91960e11b8252600482fd5b50346102b75760403660031901126102b757610d09600435611f2c61227a565b90611f39610cff82612807565b613f02565b50346102b75760203660031901126102b7576104ae60043533613d25565b50346102b757611f6b366124c2565b969095919490936001600160a01b039092169190823b156104b15791611fac939185809460405196879586948593636691f64760e01b855260048501612787565b03925af18015610d9f57611fc9575b610d86610d7a8686866129ef565b611fd48280926125d1565b6102b75780611fbb565b50346102b75760203660031901126102b7576004356001600160401b0381116104b95761200f9036906004016122ba565b61201a929192613df7565b6001600160401b0381116120d557612037816107ba600254612825565b81601f821160011461206a578190839461206494926108415750508160011b915f199060031b1c19161790565b60025580f35b60028352601f198216935f80516020615de783398151915291845b8681106120bd57508360019596106120a4575b505050811b0160025580f35b01355f19600384901b60f8161c191690555f8080612098565b90926020600181928686013581550194019101612085565b634e487b7160e01b82526041600452602482fd5b50346102b75760203660031901126102b75760206117bb600435612807565b50346102b757806003193601126102b7576020604051620186a08152f35b50346102b757610d86610d7a61213b3661246d565b916127b2565b346121ca5761214f36612317565b97999598909691959294929091906001600160a01b0316803b156121ca576121919a5f80946040519d8e9586948593636691f64760e01b855260048501612787565b03925af19687156121bf57610d8698610d7a986121af575b50612dbf565b5f6121b9916125d1565b5f6121a9565b6040513d5f823e3d90fd5b5f80fd5b346121ca575f3660031901126121ca576040517f0000000000000000000000000b144e07a0826182b6b59788c34b32bfa86fb7116001600160a01b03168152602090f35b346121ca5760203660031901126121ca576004359063ffffffff60e01b82168092036121ca57602091637965db0b60e01b8114908115612254575b5015158152f35b6301ffc9a760e01b1490508361224d565b35906001600160e01b0319821682036121ca57565b602435906001600160a01b03821682036121ca57565b600435906001600160a01b03821682036121ca57565b35906001600160a01b03821682036121ca57565b9181601f840112156121ca578235916001600160401b0383116121ca57602083818601950101116121ca57565b9181601f840112156121ca578235916001600160401b0383116121ca576020808501948460051b0101116121ca57565b60e06003198201126121ca576004356001600160a01b03811681036121ca5791602435916044356001600160401b0381116121ca5781612359916004016122ba565b929092916064356001600160401b0381116121ca578161237b916004016122e7565b929092916084356001600160401b0381116121ca578161239d916004016122e7565b9290929160a4356001600160401b0381116121ca57816123bf916004016122e7565b9290929160c435906001600160401b0382116121ca5760809082900360031901126121ca5760040190565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b602081016020825282518091526040820191602060408360051b8301019401925f915b83831061244057505050505090565b909192939460208061245e600193603f1986820301875289516123ea565b97019301930191939290612431565b60406003198201126121ca576004356001600160401b0381116121ca5781612497916004016122e7565b92909291602435906001600160401b0382116121ca5760809082900360031901126121ca5760040190565b60a06003198201126121ca576004356001600160a01b03811681036121ca5791602435916044356001600160401b0381116121ca5781612504916004016122ba565b929092916064356001600160401b0381116121ca5781612526916004016122e7565b92909291608435906001600160401b0382116121ca5760809082900360031901126121ca5760040190565b60e081019081106001600160401b0382111761256c57604052565b634e487b7160e01b5f52604160045260245ffd5b60a081019081106001600160401b0382111761256c57604052565b604081019081106001600160401b0382111761256c57604052565b606081019081106001600160401b0382111761256c57604052565b90601f801991011681019081106001600160401b0382111761256c57604052565b6001600160401b03811161256c57601f01601f191660200190565b929192612619826125f2565b9161262760405193846125d1565b8294818452818301116121ca578281602093845f960137010152565b9080601f830112156121ca5781602061265e9335910161260d565b90565b60806003198201126121ca576004356001600160401b0381116121ca578161268b916004016122e7565b929092916024356001600160401b0381116121ca57816126ad916004016122e7565b929092916044356001600160401b0381116121ca57816126cf916004016122e7565b92909291606435906001600160401b0382116121ca5760809082900360031901126121ca5760040190565b346121ca575f3660031901126121ca5760206040515f8152f35b9060406003198301126121ca576004356001600160401b0381116121ca5761016081840360031901126121ca5760040191602435906001600160401b0382116121ca57612763916004016122ba565b9091565b908060209392818452848401375f828201840152601f01601f1916010190565b60409061265e949281528160208201520191612767565b356001600160a01b03811681036121ca5790565b826060926127c2929594956129ef565b92016001600160a01b036127d58261279e565b165f5260016020526001600160601b0360405f205416806127f4575050565b6128006128059261279e565b613d25565b565b5f525f80516020615ea7833981519152602052600160405f20015490565b90600182811c92168015612853575b602083101461283f57565b634e487b7160e01b5f52602260045260245ffd5b91607f1691612834565b601f8111612869575050565b60025f5260205f20906020601f840160051c830193106128a3575b601f0160051c01905b818110612898575050565b5f815560010161288d565b9091508190612884565b6001600160401b03811161256c5760051b60200190565b903590601e19813603018212156121ca57018035906001600160401b0382116121ca576020019160608202360383136121ca57565b9190811015612909576060020190565b634e487b7160e01b5f52603260045260245ffd5b3561ffff811681036121ca5790565b8051156129095760200190565b80518210156129095760209160051b010190565b91908110156129095760051b8101359060be19813603018212156121ca570190565b6002111561297957565b634e487b7160e01b5f52602160045260245ffd5b903590601e19813603018212156121ca57018035906001600160401b0382116121ca576020019181360383136121ca57565b601f198101919082116129ce57565b634e487b7160e01b5f52601160045260245ffd5b919082039182116129ce57565b9291926129fd848383612eb2565b612a06826128ad565b93612a1460405195866125d1565b828552601f19612a23846128ad565b015f5b818110612cc857505084612a39846128ad565b612a4660405191826125d1565b848152601f19612a55866128ad565b013660208301376020830194612a6b86856128c4565b90505f5b818110612c895750505f5b818110612a8a5750505050505050565b612a9581838861294d565b90612aab612aa56060880161279e565b83614084565b90612ab68388612939565b52612c8057612ac58185612939565b5180612ad8575b50600191505b01612a7a565b606083013560028110156121ca57600190612af28161296f565b03612c7157612b04608084018461298d565b50926040840135840191612b188b8a6128c4565b90915f198101919082116129ce57612b2f926128f9565b916040612b3e6020850161279e565b930135926001600160601b0384168094036121ca57612b6060a084018461298d565b9290915a603f810290808204603f14901517156129ce57869060061c10612c62576001600160a01b031694853b156121ca5760205f8760019a612be98397612bd7996040519a8b998a98899663a12da43f60e01b885201356004870152606060248701526064860190604060208201359101612767565b84810360031901604486015291612767565b0393f19081612c52575b50612c4b577f5c5960582bfc7a494183b4e9a66bfe8ecffc07a83a48d136e732400f7b98bf5090612c22613cf6565b92612c41604051928392835260406020840152359460408301906123ea565b0390a25b5f612acc565b5050612c45565b5f612c5c916125d1565b5f612bf3565b6307099c5360e21b5f5260045ffd5b63b90a25b160e01b5f5260045ffd5b60019150612ad2565b612c9d81612c978a896128c4565b906128f9565b90600181018082116129ce57612cc161ffff612cba60019561291d565b1687612939565b5201612a6f565b806060602080938a01015201612a26565b906001600160601b03809116911601906001600160601b0382116129ce57565b80546bffffffffffffffffffffffff60601b191660609290921b6bffffffffffffffffffffffff60601b16919091179055565b908160209103126121ca575180151581036121ca5790565b9360c095919897969360ff9360e087019a60018060a01b0316875260018060a01b031660208701526040860152606085015216608083015260a08201520152565b91908110156129095760051b8101359061015e19813603018212156121ca570190565b90821015612909576127639160051b81019061298d565b919695949392905f5b818110612dde575050505061265e9394506127b2565b80612dfb8a610eef8387612df5600197898c612d85565b93612da8565b01612dc8565b903590601e19813603018212156121ca57018035906001600160401b0382116121ca57602001918160061b360383136121ca57565b91908110156129095760061b0190565b6020815260406020612e628451838386015260608501906123ea565b93015191015290565b359061ffff821682036121ca57565b35906001600160601b03821682036121ca57565b90612ea89060409396959496606084526060840191612767565b9460208201520152565b61ffff821161375157612ec4826128ad565b90612ed260405192836125d1565b828252601f19612ee1846128ad565b01366020840137612ef1836128ad565b90612eff60405192836125d1565b838252601f19612f0e856128ad565b013660208401376040850193612f248587612e01565b90505f5b8181106136975750505f5b8181106133255750505050612f4790614638565b612f60612f5760208501856128c4565b91909385612e01565b612f6f6060879693960161279e565b9160405193608085018581106001600160401b0382111761256c57604052612f96816128ad565b91612fa460405193846125d1565b81835260606020840192028101903682116121ca57915b8183106132d4575050508352612fd0816128ad565b94612fde60405196876125d1565b818652602086019160061b8101903682116121ca57915b818310613295575050506020820193845260408201928352606082019060018060a01b031681526040519260208401946020865260c08501935193608060408701528451809152602060e087019501905f5b818110613250575050505192603f19858203016060860152602080855192838152019401905f5b8181106132205750509051608085015250516001600160a01b031660a0830152819003601f19810182526020925f9290916130a990826125d1565b604051918291518091835e8101838152039060025afa156121bf575f517f0000000000000000000000000b144e07a0826182b6b59788c34b32bfa86fb7116001600160a01b0316916130fb818061298d565b843b156121ca5760405163ab750e7560e01b8152915f91839182916131479188917f6c5a03c0785e91bc0ad0db486004116010680a03af4e712bcca3188e566941009160048601612e8e565b0381875afa9081613210575b5061320b576001600160401b037f0000000000000000000000000000000000000000000000000000000069cf97ef1642116131fc57806131929161298d565b919092803b156121ca576131e1935f936040519586948593849363ab750e7560e01b85527f6c5a03c0785e91bc0ad0db486004116010680a03af4e712bcca3188e566941009160048601612e8e565b03915afa80156121bf576131f25750565b5f612805916125d1565b63439cc0cd60e01b5f5260045ffd5b505050565b5f61321a916125d1565b5f613153565b8251805161ffff1687526020908101516001600160e01b031916818801526040909601959092019160010161306e565b8251805161ffff1688526020818101516001600160a01b0316818a01526040918201516001600160601b03169189019190915260609097019690920191600101613047565b6040833603126121ca57602060409182516132af8161259b565b6132b886612e6b565b81526132c5838701612265565b83820152815201920191612ff5565b6060833603126121ca5760206060916040516132ef816125b6565b6132f886612e6b565b81526133058387016122a6565b8382015261331560408701612e7a565b6040820152815201920191612fbb565b61333081838561294d565b9060c0823603126121ca576040519160c083018381106001600160401b0382111761256c5760405280358084526020820135806020860152604083013591826040870152606084013560028110156121ca576060870190815260808501356001600160401b0381116121ca576133a99036908701612643565b906080880191825260a086019788356001600160401b0381116121ca5760209261342c9260a06133de60219436908d01612643565b91015251936133ec8561296f565b6133f58561296f565b516040519384918183019660ff60f81b9060f81b1687528051918291018484015e81015f838201520301601f1981018352826125d1565b519020916040519261343d84612580565b8684526020840192835260408401918252606084018581526080850191825260a090607460405161346e84826125d1565b818152736c66696c6c6d656e74446174614469676573742960601b608060208301927f4173736573736f72436f6d6d69746d656e742875696e7432353620696e64657884527f2c75696e743235362069642c627974657333322072657175657374446967657360408201527f742c6279746573333220636c61696d4469676573742c6279746573333220667560608201520152209551945193519051925193604051956020870197885260408701526060860152608085015283015260c082015260c0815261353e60e0826125d1565b51902061354b848a612939565b526135568388612939565b51613606576135aa937f000000000000000000000000a326b2eb45a5c3c206df905a58970dca57b8719e6001600160a01b031692613594919061298d565b9490604051956135a38761259b565b369161260d565b84526020840152803b156121ca576135d9925f916040518080968194631599ead560e01b835260048301612e46565b039161c350fa9182156121bf576001926135f6575b505b01612f33565b5f613600916125d1565b5f6135ee565b61363f937f000000000000000000000000a326b2eb45a5c3c206df905a58970dca57b8719e6001600160a01b031692613594919061298d565b84526020840152803b156121ca5761366e925f916040518080968194631599ead560e01b835260048301612e46565b03915afa9182156121bf57600192613687575b506135f0565b5f613691916125d1565b5f613681565b60206136ad826136a78a8c612e01565b90612e36565b013563ffffffff60e01b81168091036121ca576136f26136e861ffff6136e06136db866136a78f8f90612e01565b61291d565b16868861294d565b60a081019061298d565b6004929192116121ca57600161372a61ffff6137236136db878f978f6136a79163ffffffff60e01b90351699612e01565b1689612939565b5281810361373c575050600101612f28565b632e2ce35360e21b5f5260045260245260445ffd5b506377e4aa5360e11b5f5260045261ffff60245260445ffd5b919695949392905f5b818110613789575050505061265e9394506129ef565b806137a08a610eef8387612df5600197898c612d85565b01613773565b35906001600160401b03821682036121ca57565b359063ffffffff821682036121ca57565b91908260e09103126121ca576040516137e381612551565b60c08082948035845260208101356020850152613802604082016137a6565b6040850152613813606082016137ba565b6060850152613824608082016137ba565b608085015261383560a082016137ba565b60a08501520135910152565b9161385a91833560201c6001600160a01b031684614735565b50906040613899611bbe61388961387085614d43565b90506001600160401b03429116109460803691016137cb565b6001600160401b03421690614ddf565b6001600160601b038251916138ad836125b6565b60018352602083018590521691018190526001607f1b91156138d5576001607e1b5b1717905d565b5f6138cf565b6138e7613904916143eb565b6001600160a01b039091165f908152600160205260409020614434565b5090565b604051905f825f80516020615e07833981519152549161392783612825565b80835292600181169081156139b6575060011461394b575b612805925003836125d1565b505f80516020615e078339815191525f90815290917f42ad5d3e1f2e6e70edcf6d991b8a3023d3fca8047a131592f9edb9fd9b89d57d5b81831061399a5750509060206128059282010161393f565b6020919350806001915483858901015201910190918492613982565b6020925061280594915060ff191682840152151560051b82010161393f565b604051905f825f80516020615e2783398151915254916139f483612825565b80835292600181169081156139b65750600114613a1757612805925003836125d1565b505f80516020615e278339815191525f90815290917f5f9ce34815f8e11431c7bb75a8e6886a91478f7ffc1dbb0a98dc240fddd76b755b818310613a665750509060206128059282010161393f565b6020919350806001915483858901015201910190918492613a4e565b613a8b346144e2565b335f5260016020526001600160601b03613aac60405f209282845416612cd9565b166001600160601b03198254161790556040513481527fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c60203392a2565b9035603e19823603018112156121ca570190565b9060038210156129795752565b9035601e19823603018112156121ca5701602081359101916001600160401b0382116121ca5781360383136121ca57565b90813581526020820135607e19833603018112156121ca57610160602083015282016001600160a01b03613b6f826122a6565b166101608301526001600160601b03613b8a60208301612e7a565b16610180830152613b9e6040820182613aea565b9060806101a084015281359160038310156121ca57613bd6613be991613bcc613c22956101e0880190613afe565b6020810190613b0b565b6040610200870152610220860191612767565b906001600160e01b031990613c0090606001612265565b166101c0840152613c146040850185613b0b565b908483036040860152612767565b613c2f6060840184613aea565b8282036060840152803560028110156121ca57610140926040613c66859484613c5a613c769661296f565b84526020810190613b0b565b9190928160208201520191612767565b936080810135608085015260a081013560a08501526001600160401b03613c9f60c083016137a6565b1660c085015263ffffffff613cb660e083016137ba565b1660e085015263ffffffff613cce61010083016137ba565b1661010085015263ffffffff613ce761012083016137ba565b16610120850152013591015290565b3d15613d20573d90613d07826125f2565b91613d1560405193846125d1565b82523d5f602084013e565b606090565b9060018060a01b03821691825f5260016020526001600160601b0360405f2054166001600160601b03613d57846144e2565b1611613de4575f8080848194613d6c826144e2565b88845260016020526001600160601b03806040862092818454160316166001600160601b03198254161790555af1613da2613cf6565b5015613dd55760207f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b6591604051908152a2565b6312171d8360e31b5f5260045ffd5b8263112fed8b60e31b5f5260045260245ffd5b335f9081525f80516020615e67833981519152602052604090205460ff1615613e1c57565b63e2517d3f60e01b5f52336004525f60245260445ffd5b5f8181525f80516020615ea78339815191526020908152604080832033845290915290205460ff1615613e635750565b63e2517d3f60e01b5f523360045260245260445ffd5b6001600160a01b0381165f9081525f80516020615e67833981519152602052604090205460ff16613efd576001600160a01b03165f8181525f80516020615e6783398151915260205260408120805460ff191660011790553391907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d8180a4600190565b505f90565b5f8181525f80516020615ea7833981519152602090815260408083206001600160a01b038616845290915290205460ff16613fa0575f8181525f80516020615ea7833981519152602090815260408083206001600160a01b0395909516808452949091528120805460ff19166001179055339291907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d9080a4600190565b50505f90565b5f8181525f80516020615ea7833981519152602090815260408083206001600160a01b038616845290915290205460ff1615613fa0575f8181525f80516020615ea7833981519152602090815260408083206001600160a01b0395909516808452949091528120805460ff19169055339291907ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9080a4600190565b906001600160401b03809116911601906001600160401b0382116129ce57565b61265e9062ffffff60406001600160401b036020840151169201511690614042565b90916060925f92803590614097826143eb565b969060018060a01b0381165f5260016020526140b68860405f20614434565b91819991936040516140c781612551565b5f81525f60208201525f60408201525f828201525f60808201525f60a08201525f60c08201529a61436f575b5060208501359961410261553d565b508a5c9461410e61553d565b506040516001607f1b87161515614124826125b6565b8082526001600160601b03604060208401936001607e1b8b161515855201981688525f1461431c57516142ac5791878995949288945b156142945760208101516001600160401b031642116142775761417d97506158f1565b955b8651614239575b604051906020825283602083015260408201526040820135606082015260608201359160028310156121ca576142348291846141e27faf1db8f86d3f32029a484ff54c7ac1d7ef8f038ab050fc065af9e82eb9b850ca9661296f565b608084015261421661420b6141fa6080840184613b0b565b60c060a088015260e0870191612767565b9160a0810190613b0b565b848303601f190160c08601526001600160a01b039098169790612767565b0390a3565b7f210e4fd706e561df48472433bcc50b4589f2c13e784e9992f4c3e6de26eb3564604051602081528061426f602082018b6123ea565b0390a1614186565b9291906001600160601b0361428e98511693615697565b9561417f565b5050906001600160601b0361428e965116918861555b565b5050505050505092505091506040519063873fd26b60e01b60208301526024820152602481526142dd6044826125d1565b7f210e4fd706e561df48472433bcc50b4589f2c13e784e9992f4c3e6de26eb3564604051602081528061431360208201856123ea565b0390a190600190565b508080614362575b1561434f5761433282614062565b6001600160401b03429116106142ac57918789959492889461415a565b8763c274d3e360e01b5f5260045260245ffd5b508b60c083015114614324565b909950855f525f602052600260405f206001600160601b036040519361439485612551565b825460018060a01b03811686526001600160401b038160a01c16602087015262ffffff8160e01c16604087015260f81c8186015260018301549082821660808701521c1660a0840152015460c0820152985f6140f3565b906001600160c11b0319821661441357602082901c6001600160a01b03169163ffffffff1690565b6341abc80160e01b5f5260045ffd5b63020000008210156129095701905f90565b63ffffffff821691906020831015614486576401fffffffe905460c01c9160011b1691808304600214901517156129ce576001600160401b03906003831b1616901c9060026001831615159216151590565b9161449191506129bf565b908160011b91808304600214811517156129ce5760ff916144c19160071c6001600160f81b031690600101614422565b90549060031b1c9116906003821b16901c9060026001831615159216151590565b6001600160601b0381116144fc576001600160601b031690565b6306dfcc6560e41b5f52606060045260245260445ffd5b6040516323b872dd60e01b81526001600160a01b039182166004820152306024820152604481018490529192917f000000000000000000000000aa61bb7777bd01b684347961918f1e07fbbce7cf909116906020905f9060649082855af19081601f3d1160015f511416151661462b575b50156145ef576020816145e66145ba7ff645c19720906ca336d36d26058a9489c6c757fe35843b75a74e3b8aa972ecf5946144e2565b9460018060a01b031694855f5260018452611c0e60405f20916001600160601b03835460601c16612cd9565b604051908152a2565b60405162461bcd60e51b81526020600482015260146024820152731514905394d1915497d19493d357d1905253115160621b6044820152606490fd5b3b153d171590505f614584565b80511561441357600181511461472c5780515b60018111614661575061465d9061292c565b5190565b600181018082116129ce5760011c905f5b8160011c81106146c0575060018082161461468e575b5061464b565b5f1981019081116129ce576146a39083612939565b515f1982018281116129ce576146b99084612939565b525f614688565b600181901b906001600160ff1b03811681036129ce576146e08286612939565b51600183018093116129ce576146f860019387612939565b51908181101561471d575f5260205260405f205b6147168287612939565b5201614672565b905f5260205260405f2061470c565b61465d9061292c565b91939290610160833603126121ca5760405161475081612580565b83359384825260208101356001600160401b0381116121ca5781019081360391608083126121ca576040805193614786856125b6565b126121ca576040516147978161259b565b6147a0826122a6565b81526147ae60208301612e7a565b6020820152835260408101356001600160401b0381116121ca5781016040813603126121ca57604051916147e18361259b565b813560038110156121ca5783526020820135926001600160401b0384116121ca5761481460609361482495369101612643565b6020820152602086015201612265565b60408301526020830191825260408101356001600160401b0381116121ca57810136601f820112156121ca5761486190369060208135910161260d565b906040840191825260608101356001600160401b0381116121ca578101906040823603126121ca57604051916148968361259b565b803560028110156121ca57835260208101356001600160401b0381116121ca576148c291369101612643565b6020830152606085019182526148dc9036906080016137cb565b90608085019182526148ec615449565b6148f46152af565b6148fc6152f9565b9061490561533e565b61490d6153fc565b6149156154d0565b916040519485946020860197805160208192018a5e860160208101915f83528051926020849201905e016020015f815281516020819301825e015f815281516020819301825e015f815281516020819301825e015f815281516020819301825e015f815203601f198101825261498b90826125d1565b5190209451935161499a6154d0565b6149a26152af565b6149aa6153fc565b90604051918291602083019480516020819201875e830160208101915f83528051926020849201905e016020015f815281516020819301825e015f815203601f19810182526149f990826125d1565b519020908051614a076152af565b8051906020012090600160a01b6001900381511690602001516001600160601b031660405191602083019384526040830152606082015260608152614a4d6080826125d1565b519020906020810151614a5e6153fc565b80519060200120908051906003821015612979576020015160208151910120614a9560405192602084019485526040840190613afe565b606082015260608152614aa96080826125d1565b51902090604063ffffffff60e01b9101511690604051926020840194855260408401526060830152608082015260808152614ae560a0826125d1565b5190209251602081519101209051614afb6152f9565b60208151910120906020815191614b118361296f565b0151602081519101206040519160208301938452614b2e8161296f565b6040830152606082015260608152614b476080826125d1565b5190209151614b5461533e565b604051614b806020828180820195805191829101875e81015f838201520301601f1981018352826125d1565b519020908051906020810151906001600160401b0360408201511663ffffffff60608301511663ffffffff6080840151169160c063ffffffff60a08601511694015194604051966020880198895260408801526060870152608086015260a085015260c084015260e08301526101008201526101008152614c03610120826125d1565b51902092604051946020860196875260408601526060850152608084015260a083015260c082015260c08152614c3a60e0826125d1565b51902094614c4f86614c4a615a7e565b615b33565b93600160c01b1615614d0c5791602091614c8093604051809581948293630b135d3f60e11b84528960048501612787565b03916001600160a01b0316620186a0fa9081156121bf575f91614cc9575b506001600160e01b0319166374eca2c160e11b01614cba579190565b638baa579f60e01b5f5260045ffd5b90506020813d602011614d04575b81614ce4602093836125d1565b810103126121ca57516001600160e01b0319811681036121ca575f614c9e565b3d9150614cd7565b614d1e614d2491614d2d94369161260d565b84615b50565b90939193615b8a565b6001600160a01b03908116911603614cba579190565b614d519060803691016137cb565b9081516020830151106144135763ffffffff606083015116608083019063ffffffff825116106144135763ffffffff90511660a083019063ffffffff8251161061441357614dbe9063ffffffff6001600160401b036040614db187615ae5565b9601511691511690614042565b9162ffffff6001600160401b03614dd58386614ec3565b1611614413579190565b9060408201906001600160401b0380835116911690811115614ebd576001600160401b03614e0c84615ae5565b168111614eb6576001600160401b03825116906001600160401b03614e3d606086019363ffffffff85511690614042565b16811115614e4f575050506020015190565b614e7c906001600160401b0363ffffffff614e7060208801518851906129e2565b945116945116906129e2565b9251928181029181830414901517156129ce578115614ea2570481018091116129ce5790565b634e487b7160e01b5f52601260045260245ffd5b5050505f90565b50505190565b906001600160401b03809116911603906001600160401b0382116129ce57565b9590929796949360018060a01b031697885f526001602052614f088560405f20614434565b9061529b57615287576001600160401b0386169889421161526f57614f36611bbe6138893660808c016137cb565b96815f52600160205260405f20996001600160601b038b5416946001600160601b038a169384871061525d575060018060a01b031698895f52600160205260405f20906001600160601b03825460601c16966101408d013580981061524a57918d6001600160601b0380614fdc94614fe19897960316166001600160601b03198254161790556001600160601b03614fcd896144e2565b81835460601c16031690612cf9565b614ec3565b926001600160401b03841662ffffff81116152335750615000906144e2565b6040519361500d85612551565b88855260208086019c8d5262ffffff90911660408087019182525f60608801818152608089019687526001600160601b0390951660a0808a0191825260c08a019889528e35808452958390529290912097519e51925194519290911b67ffffffffffffffff60a01b166001600160a01b039e909e169d909d1760e09390931b62ffffff60e01b169290921760f89290921b6001600160f81b031916919091178455996001840191516001600160601b03166001600160601b03166001600160601b0319835416178255516001600160601b03166150e991612cf9565b51906002015563ffffffff831692602084105f146151a4576401fffffffe9060011b1692808404600214901517156129ce5785546001600160c01b038116600190941b6001600160401b031660c091821c17901b6001600160c01b031916929092179094557fe5e43c93dc0ec595ed3b122bdc6d39a480e9d17fb6812e0f90cfc4ba33b0969e9361519f915b6151916040519586958652606060208701526060860190613b3c565b918483036040860152612767565b0390a2565b50916151af906129bf565b918260011b95838704600214841517156129ce577fe5e43c93dc0ec595ed3b122bdc6d39a480e9d17fb6812e0f90cfc4ba33b0969e9661519f9461522e9260ff9160019161520b9160071c6001600160f81b0316908301614422565b929093161b82548260031b1c179082549060031b91821b915f19901b1916179055565b615175565b6306dfcc6560e41b5f52601860045260245260445ffd5b8b63112fed8b60e31b5f5260045260245ffd5b63112fed8b60e31b5f5260045260245ffd5b898863cfe6a8fd60e01b5f523560045260245260445ffd5b86631cfdeebb60e01b5f523560045260245ffd5b8763a905765160e01b5f523560045260245ffd5b604051906152be6060836125d1565b60268252654c696d69742960d01b6040837f43616c6c6261636b286164647265737320616464722c75696e7439362067617360208201520152565b604051906153086060836125d1565b60218252602960f81b6040837f496e7075742875696e743820696e707574547970652c6279746573206461746160208201520152565b6040519061534d60c0836125d1565b60888252676c61746572616c2960c01b60a0837f4f666665722875696e74323536206d696e50726963652c75696e74323536206d60208201527f617850726963652c75696e7436342072616d70557053746172742c75696e743360408201527f322072616d705570506572696f642c75696e743332206c6f636b54696d656f7560608201527f742c75696e7433322074696d656f75742c75696e74323536206c6f636b436f6c60808201520152565b6040519061540b6060836125d1565b602982526874657320646174612960b81b6040837f5072656469636174652875696e743820707265646963617465547970652c627960208201520152565b604051906154586080836125d1565b605a82527f6c2c496e70757420696e7075742c4f66666572206f66666572290000000000006060837f50726f6f66526571756573742875696e743235362069642c526571756972656d60208201527f656e747320726571756972656d656e74732c737472696e6720696d616765557260408201520152565b604051906154df6080836125d1565b60438252626f722960e81b6060837f526571756972656d656e74732843616c6c6261636b2063616c6c6261636b2c5060208201527f7265646963617465207072656469636174652c6279746573342073656c65637460408201520152565b6040519061554a826125b6565b5f6040838281528260208201520152565b969495919293909660609661564a575f80516020615f0783398151915260209596979860018060a01b031693845f526001875261559c60405f209687615c73565b6040519387013584526001600160a01b0316958693a36001600160601b03825416906001600160601b038516821061561e57506001600160601b038481920316166001600160601b03198254161790555f5260016020526001600160601b0361560c60405f209282845416612cd9565b166001600160601b0319825416179055565b949550505050506040519063112fed8b60e31b602083015260248201526024815261265e6044826125d1565b955050505050915060405190631cfdeebb60e01b602083015260248201526024815261265e6044826125d1565b906001600160601b03809116911603906001600160601b0382116129ce57565b93959796929490946060986001606087015116151580156158e1575b6158b25715615861575b50506001600160a01b03165f908152600160205260408120608093909301516001600160601b03868116969592949116858188111561582e578161570091615677565b906001600160601b03835416906001600160601b0383168210615809575b5082546bffffffffffffffffffffffff19169190036001600160601b03161790555b5f90815260208190526040902080546affffffffffffffffffffff60a01b81166001600160a01b0384169081176001600160a01b0319929092161760f890811c600217901b6001600160f81b03191617905560018060a01b03165f52600160205260405f206001600160601b036157ba8482845416612cd9565b166001600160601b03198254161790556157d2575050565b6001600160601b039192935060405192636008fdcb60e01b602085015260248401521660448201526044815261265e6064826125d1565b96509450506001600160601b0380615822868098612cd9565b9660019691509161571e565b61584361584c916001600160601b0393615677565b82845416612cd9565b166001600160601b0319825416179055615740565b6001600160a01b0383165f9081526001602052604090206158829190615c73565b60405160209182013581526001600160a01b0384169186915f80516020615f078339815191529190a35f806156bd565b5050505050509192505060405190631cfdeebb60e01b602083015260248201526024815261265e6044826125d1565b50600260608701511615156156b3565b9391909296959496606097600160608701511615158015615a6e575b615a4057156159f5575b505082516001600160a01b0394851694168414801591906159e1575b506159b75760a061280593926001600160601b03925f525f6020525f6001604082208160f81b828060f81b03825416178155015582608082015116845f5260016020528361598860405f209282845416612cd9565b168419825416179055015116905f526001602052611c0e60405f20916001600160601b03835460601c16612cd9565b92935050506040519063a905765160e01b602083015260248201526024815261265e6044826125d1565b9050602060c084015191013514155f615933565b615a119160018060a01b03165f52600160205260405f20615c73565b60405160208281013582526001600160a01b0386169184915f80516020615f0783398151915291a35f80615917565b50505050929350505060405190631cfdeebb60e01b602083015260248201526024815261265e6044826125d1565b506002606087015116151561590d565b615a86615bea565b615a8e615c41565b6040519060208201927f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8452604083015260608201524660808201523060a082015260a08152615adf60c0826125d1565b51902090565b61265e9063ffffffff60806001600160401b036040840151169201511690614042565b60ff5f80516020615ec78339815191525460401c1615615b2457565b631afcd79f60e31b5f5260045ffd5b6042916040519161190160f01b8352600283015260228201522090565b8151919060418303615b8057615b799250602082015190606060408401519301515f1a90615d10565b9192909190565b50505f9160029190565b60048110156129795780615b9c575050565b60018103615bb35763f645eedf60e01b5f5260045ffd5b60028103615bce575063fce698f760e01b5f5260045260245ffd5b600314615bd85750565b6335e2f38360e21b5f5260045260245ffd5b615bf2613908565b8051908115615c02576020012090565b50505f80516020615e47833981519152548015615c1c5790565b507fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a47090565b615c496139d5565b8051908115615c59576020012090565b50505f80516020615ee7833981519152548015615c1c5790565b9063ffffffff8116906020821015615cd0576401fffffffe9060011b1690808204600214901517156129ce5781546001600160c01b038116600290921b6001600160401b031660c091821c17901b6001600160c01b031916179055565b50615cda906129bf565b8060011b90808204600214811517156129ce576128059260ff9160029161520b9160071c6001600160f81b031690600101614422565b91906fa2a8918ca85bafe22016d0b997e4df60600160ff1b038411615d7d579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa156121bf575f516001600160a01b03811615615d7357905f905f90565b505f906001905f90565b5050505f9160039190565b90615dac5750805115615d9d57602081519101fd5b63d6bda27560e01b5f5260045ffd5b81511580615ddd575b615dbd575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b15615db556fe405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5acea16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d102a16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d103a16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100b7db2dd08fcb62d0c9e08c51941cae53c267786a0b75803fb7960902fc8ef97d360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00a16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d101120ea8d7610aa46e4a31b254c5d07489ebe8f1a93dc7bbbe60eaf3db2c62c0cca164736f6c634300081a000a diff --git a/contracts/test/legacy/deployed-bytecode.meta.toml b/contracts/test/legacy/deployed-bytecode.meta.toml new file mode 100644 index 000000000..a356173b1 --- /dev/null +++ b/contracts/test/legacy/deployed-bytecode.meta.toml @@ -0,0 +1,34 @@ +# Reference snapshot of the BoundlessMarket implementation currently +# deployed at the Base mainnet proxy. Used by +# `scripts/verify-legacy-bytecode.py` to assert that contracts/src/legacy/ +# compiles to the same bytecode (modulo immutable slots that are baked in at +# deploy time). +# +# Refresh procedure: +# cast code 0x22bb6bbe5d221ef3e738029dab4d1d27ec725cd3 \ +# --rpc-url https://base.drpc.org \ +# > contracts/test/legacy/deployed-bytecode.hex +# and update `fetched_at_block` below. + +network = "base-mainnet" +chain_id = 8453 +proxy = "0xfd152dadc5183870710fe54f939eae3ab9f0fe82" +impl = "0x22bb6bbe5d221ef3e738029dab4d1d27ec725cd3" +fetched_at_block = 46576272 + +# Constructor immutables baked into the deployed bytecode. The compiled +# legacy/ artifact has zeros at these positions; the verification script +# masks them out for the body-match check and then re-extracts each baked-in +# value and asserts it matches the expected value below. Mismatch on either +# check means the legacy/ source or this metadata has drifted from production. +# +# Types: address values are 20 bytes; bytes32 values are 32 bytes; uint64 +# values are decoded as integers. All addresses are lowercase-normalized +# before comparison. +[immutables] +VERIFIER = "0x0b144E07A0826182B6b59788c34b32Bfa86Fb711" +ASSESSOR_ID = "0x6c5a03c0785e91bc0ad0db486004116010680a03af4e712bcca3188e56694100" +COLLATERAL_TOKEN_CONTRACT = "0xAA61bB7777bD01B684347961918f1E07fBbCe7CF" +DEPRECATED_ASSESSOR_ID = "0x6c5a03c0785e91bc0ad0db486004116010680a03af4e712bcca3188e56694100" +DEPRECATED_ASSESSOR_EXPIRES_AT = 1775212527 +APPLICATION_VERIFIER = "0xA326b2eb45A5C3C206dF905A58970DcA57B8719e" diff --git a/crates/boundless-market/build.rs b/crates/boundless-market/build.rs index c7780b067..972b8a390 100644 --- a/crates/boundless-market/build.rs +++ b/crates/boundless-market/build.rs @@ -269,7 +269,7 @@ fn get_interfaces(contract: &str) -> &str { "constructor(address verifier, bytes32 imageId, string memory imageUrl) {}" } "BoundlessMarket" => { - r#"constructor(address router, address collateralTokenContract) {} + r#"constructor(address router, address collateralTokenContract, address legacyImpl) {} function initialize(address initialOwner) {}"# } "ERC1967Proxy" => "constructor(address implementation, bytes memory data) payable {}", diff --git a/crates/boundless-market/src/contracts/bytecode.rs b/crates/boundless-market/src/contracts/bytecode.rs index 1a35a5c13..3890fa8f7 100644 --- a/crates/boundless-market/src/contracts/bytecode.rs +++ b/crates/boundless-market/src/contracts/bytecode.rs @@ -1,9 +1,9 @@ // Auto-generated file, do not edit manually alloy::sol! { - #[sol(rpc, bytecode = "60e0346101b357601f61549f38819003918201601f19168301916001600160401b038311848410176101b75780849260409485528339810103126101b35780516001600160a01b038116918282036101b35760200151916001600160a01b038316908184036101b35730608052156101a457156101955760a05260c0527ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005460ff8160401c16610186576002600160401b03196001600160401b0382160161011d575b6040516152d390816101cc82396080518181816113960152611427015260a051818181611acc015261228e015260c0518181816104920152818161058b01528181611131015281816112b6015281816117f4015261345b0152f35b6001600160401b0319166001600160401b039081177ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290602090a15f6100c2565b63f92ee8a960e01b5f5260045ffd5b633a001e0560e11b5f5260045ffd5b63466d7fef60e01b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe6080806040526004361015610012575f80fd5b5f905f3560e01c90816301ffc9a714611bf157508063122bf11814611bb55780631472e47914611b9e5780631ce0302414611b81578063248a9ca314611b635780632e1a7d4d14611b465780632f2ff15d14611b15578063329264ab14611afb57806332fe7b2614611ab757806336568abe14611a735780633f3e2c0d14611a3757806341451f941461197657806345bc4d10146116115780634cefb7cf146115eb5780634f1ef286146113ea57806352d1902d14611384578063553c02481461136a5780635b07fdd8146113485780635d704b331461129257806360dfd4a9146111fa5780636112fe2e14611099578063672b01941461106a57806370a082311461102757806375b238fc14610e0057806379965fdf1461100f57806381bf6c2414610fc657806384b0196e14610e9e57806391d1485414610e48578063956b096014610e2b5780639c7a8c6114610e05578063a217fddf14610e00578063ad3cb1cc14610db7578063ae7330f114610d70578063b09c980b14610d2a578063b760faf914610ca4578063bad4a01f14610c85578063c4d66de8146107b8578063c515c15f14610733578063c64067a21461071b578063cb74db11146106f2578063d0e30db0146106de578063d547741f146106a3578063dbfb7e7e1461066a578063df2e6706146105f8578063eba2ecc8146105ba578063ef1ae1c814610575578063f2800f1a1461051e578063fd737ea814610465578063ff1214a5146102625763ffa1ad7414610244575f80fd5b3461025f578060031936011261025f57602060405160018152f35b80fd5b503461025f57606036600319011261025f576004356001600160401b0381116104615761016081600401916003199036030112610461576024356001600160401b03811161045d576102b8903690600401611d4c565b916044356001600160401b038111610459576102d8903690600401611d4c565b6102e28335613304565b916102ef878784886137db565b604051919591610300606082611ecc565b60218152602081017f4c6f636b526571756573742850726f6f66526571756573742072657175657374815260408201602960f81b905261033e614166565b906103476141b0565b8d6103506141f5565b6103586142b3565b610360614300565b91610369614387565b94604051978897602089019a5180918c5e880160208101918783528051926020849201905e0160200185815281516020819301825e0184815281516020819301825e0183815281516020819301825e0182815281516020819301825e0190815281516020819301825e018d815203601f19810182526103e89082611ecc565b51902090604051906020820192835260408201526040815261040b606082611ecc565b519020610416614827565b90610420916148d6565b91369061042c92611f08565b610435916148f3565b6104419195929561492d565b61044a85613bfc565b96610456989196613d9a565b80f35b8480fd5b8280fd5b5080fd5b503461025f5760c036600319011261025f5761047f611d22565b6024358260643560ff81168103610461577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316803b1561045d5760405163d505accf60e01b815291839183918290849082906104f49060a43590608435906044358d303360048901612b09565b03925af1610509575b5050610456913361342c565b8161051391611ecc565b61045d57825f6104fd565b503461025f57602036600319011261025f576004359061053d82612c7f565b15610563576040816020936001600160401b039352808452205460a01c16604051908152f35b60249163d2be005d60e01b8252600452fd5b503461025f578060031936011261025f576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b503461025f576104566105cc36611fa8565b916105d78135613304565b906105e4858583866137db565b506105ee84613bfc565b9690953395613d9a565b507fc354af001adff0e8c35481c5ce3df3edee370c71572514d281e884c8cb55220361062336611fa8565b929190923461065d575b610657604051928392604084526106476040850183612d14565b9184830360208601523596612120565b0390a280f35b610665612cac565b61062d565b503461025f5761069f61069361068e61068236611d79565b95939094929192612f5d565b612240565b60405191829182611cad565b0390f35b503461025f57604036600319011261025f576106da6004356106c3611d0c565b906106d56106d0826129e1565b6130b3565b613226565b5080f35b508060031936011261025f57610456612cac565b503461025f57602036600319011261025f576020610711600435612c7f565b6040519015158152f35b503461025f5761045661072d36611fa8565b91612be5565b503461025f57602036600319011261025f57604060e091600435815280602052208054906001600160601b0360026001830154920154916040519360018060a01b03811685526001600160401b038160a01c16602086015262ffffff81871c16604086015260f81c6060850152818116608085015260601c1660a083015260c0820152f35b503461025f57602036600319011261025f576107d2611d22565b5f805160206152678339815191525460ff8160401c1615906001600160401b03811680159081610c7d575b6001149081610c73575b159081610c6a575b50610c5b5767ffffffffffffffff1981166001175f805160206152678339815191525581610c2f575b506001600160a01b03821615610c2057610850614888565b610858614888565b60409182516108678482611ecc565b601081526f12509bdd5b991b195cdcd3585c9ad95d60821b60208201528351906108918583611ecc565b60018252603160f81b60208301526108a7614888565b6108af614888565b8051906001600160401b038211610c0c5781906108d95f805160206151a783398151915254613629565b601f8111610b92575b50602090601f8311600114610b16578892610b0b575b50508160011b915f199060031b1c1916175f805160206151a7833981519152555b8051906001600160401b038211610af7576109415f805160206151c783398151915254613629565b601f8111610a88575b50602090601f8311600114610a08576109ad9392918791836109fd575b50508160011b915f199060031b1c1916175f805160206151c7833981519152555b845f805160206151e783398151915255845f80516020615287833981519152556130f9565b506109b6575080f35b5f80516020615267833981519152805460ff60401b1916905551600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290602090a180f35b015190505f80610967565b5f805160206151c783398151915287528187209190601f198416885b818110610a7057509160019391856109ad97969410610a58575b505050811b015f805160206151c783398151915255610988565b01515f1960f88460031b161c191690555f8080610a3e565b92936020600181928786015181550195019301610a24565b5f805160206151c783398151915287527f5f9ce34815f8e11431c7bb75a8e6886a91478f7ffc1dbb0a98dc240fddd76b75601f840160051c81019160208510610aed575b601f0160051c01905b818110610ae2575061094a565b878155600101610ad5565b9091508190610acc565b634e487b7160e01b86526041600452602486fd5b015190505f806108f8565b5f805160206151a783398151915289528189209250601f198416895b818110610b7a5750908460019594939210610b62575b505050811b015f805160206151a783398151915255610919565b01515f1960f88460031b161c191690555f8080610b48565b92936020600181928786015181550195019301610b32565b5f805160206151a783398151915289529091507f42ad5d3e1f2e6e70edcf6d991b8a3023d3fca8047a131592f9edb9fd9b89d57d601f840160051c81019160208510610c02575b90601f859493920160051c01905b818110610bf457506108e2565b898155849350600101610be7565b9091508190610bd9565b634e487b7160e01b87526041600452602487fd5b63267eaa8160e21b8352600483fd5b68ffffffffffffffffff191668010000000000000001175f80516020615267833981519152555f610838565b63f92ee8a960e01b8452600484fd5b9050155f61080f565b303b159150610807565b8391506107fd565b503461025f57602036600319011261025f57610456600435333361342c565b50602036600319011261025f57610cb9611d22565b610cc2346133fb565b9060018060a01b03169081835260016020526001600160601b03610ced604085209282845416612a9e565b166001600160601b03198254161790557fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c6020604051348152a280f35b503461025f57602036600319011261025f576020906001600160601b03906040906001600160a01b03610d5b611d22565b16815260018452205460601c16604051908152f35b503461025f57606036600319011261025f57610d8a611d22565b604435906001600160401b03821161045d57610dad610456923690600401611d4c565b9160243590612f5d565b503461025f578060031936011261025f575061069f604051610dda604082611ecc565b60058152640352e302e360dc1b6020820152604051918291602083526020830190611c89565b61136a565b503461025f5761069f610693610e26610e1d36611f5c565b93919092613573565b612a13565b503461025f578060031936011261025f5760206040516113888152f35b503461025f57604036600319011261025f576040610e64611d0c565b9160043581525f80516020615247833981519152602052209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b503461025f578060031936011261025f575f805160206151e7833981519152541580610fb0575b15610f7357610f1790610ed6613661565b90610edf61372e565b906020610f2560405193610ef38386611ecc565b8385525f368137604051968796600f60f81b885260e08589015260e0880190611c89565b908682036040880152611c89565b904660608601523060808601528260a086015284820360c08601528080855193848152019401925b828110610f5c57505050500390f35b835185528695509381019392810192600101610f4d565b60405162461bcd60e51b81526020600482015260156024820152741152540dcc4c8e88155b9a5b9a5d1a585b1a5e9959605a1b6044820152606490fd5b505f805160206152878339815191525415610ec5565b503461025f57602036600319011261025f576110036020916040610feb600435613304565b6001600160a01b03909116835260018552912061334d565b90506040519015158152f35b503461025f5761069f61069361068e610e1d36611f5c565b503461025f57602036600319011261025f576020906001600160601b03906040906001600160a01b03611058611d22565b16815260018452205416604051908152f35b503461025f5761069f610693610e2661109461108536611de1565b98969793929491959097612f5d565b613573565b503461025f57602036600319011261025f5760043533825260016020526001600160601b03604083205460601c166001600160601b036110d8836133fb565b16116111e75761110e6110ea826133fb565b33845260016020526001600160601b03604085209181835460601c16031690612abe565b60405163a9059cbb60e01b815233600482015260248101829052602081604481867f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03165af19081156111dc5783916111ad575b501561119e576040519081527fa315121c7f539fd811176ad2735d5d3981237b261889ec13ae4d617ad06e39bc60203392a280f35b6312171d8360e31b8252600482fd5b6111cf915060203d6020116111d5575b6111c78183611ecc565b810190612af1565b5f611169565b503d6111bd565b6040513d85823e3d90fd5b63112fed8b60e31b825233600452602482fd5b503461025f57602036600319011261025f57600460606040602093833581528085522060026040519161122c83611e67565b805460018060a01b03811684526001600160401b038160a01c168785015262ffffff8160e01c16604085015260f81c848401526001600160601b0360018201548181166080860152851c1660a0840152015460c082015201511615156040519015158152f35b50346113445760a03660031901126113445760043560443560ff81168103611344577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316803b156113445760405163d505accf60e01b8152915f9183918290849082906113189060843590606435906024358c303360048901612b09565b03925af161132d575b5061045690333361342c565b61133a9192505f90611ecc565b5f90610456611321565b5f80fd5b34611344575f366003190112611344576020611362614827565b604051908152f35b34611344575f3660031901126113445760206040515f8152f35b34611344575f366003190112611344577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031630036113db5760206040515f805160206152278339815191528152f35b63703e46dd60e11b5f5260045ffd5b6040366003190112611344576113fe611d22565b6024356001600160401b0381116113445761141d903690600401611f3e565b6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163081149081156115c9575b506113db57335f9081525f80516020615207833981519152602052604090205460ff16156115b2576040516352d1902d60e01b81526001600160a01b0383169290602081600481875afa5f918161157e575b506114bc5783634c9c8ce360e01b5f5260045260245ffd5b805f8051602061522783398151915285920361156c5750813b1561155a575f8051602061522783398151915280546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a2815115611542575f8083602061154095519101845af461153a612fb2565b91615148565b005b50503461154b57005b63b398979f60e01b5f5260045ffd5b634c9c8ce360e01b5f5260045260245ffd5b632a87526960e21b5f5260045260245ffd5b9091506020813d6020116115aa575b8161159a60209383611ecc565b81010312611344575190856114a4565b3d915061158d565b63e2517d3f60e01b5f52336004525f60245260445ffd5b5f80516020615227833981519152546001600160a01b03161415905083611452565b3461134457604036600319011261134457611540611607611d22565b602435903361342c565b346113445760203660031901126113445760043561164d61163182613304565b919060018060a01b031691825f52600160205260405f2061334d565b501561196357815f525f60205260405f206040519061166b82611e67565b805460018060a01b03811683526001600160401b038160a01c16602084015262ffffff8160e01c16604084015260f81c6060830152600181015490600260808401916001600160601b03841683526001600160601b0360a086019460601c168452015460c0840152600460608401511661195057600160608401511661193d576001600160401b036116fc846132e2565b16421115611914575f85815260208190526040812080546001600160f81b03811660f891821c60041790911b6001600160f81b031916178155600101556001600160601b038251169161138883029280840461138814901517156119005761177861177d916127106001600160601b0395049485915116612a91565b6133fb565b936002606060018060a01b038651169501511615155f1461189c57505060018060a01b0382165f5260016020526117ce60405f206117c8856001600160601b03835460601c16612a9e565b90612abe565b60405163a9059cbb60e01b815261dead600482015260248101829052916020836044815f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03165af18015611891577f79ca7c80cf57b513ffdf8aa37ec70e40757f5e0d35219241860bb4b4c2fa7616946060946001600160601b0392611874575b5060405193845216602083015260018060a01b03166040820152a2005b61188c9060203d6020116111d5576111c78183611ecc565b611857565b6040513d5f823e3d90fd5b9092506001600160601b033093305f5260016020526118c860405f206117c88885835460601c16612a9e565b5116905f5260016020526001600160601b036118eb60405f209282845416612a9e565b166001600160601b03198254161790556117ce565b634e487b7160e01b5f52601160045260245ffd5b6001600160401b0385611926856132e2565b9063079c66ab60e41b5f526004521660245260445ffd5b84631cfdeebb60e01b5f5260045260245ffd5b84633231064d60e11b5f5260045260245ffd5b5063d2be005d60e01b5f5260045260245ffd5b346113445760203660031901126113445760043561199381612c7f565b15611a25575f525f6020526020611a1460405f206002604051916119b683611e67565b805460018060a01b03811684526001600160401b038160a01c168685015262ffffff8160e01c16604085015260f81c60608401526001600160601b036001820154818116608086015260601c1660a0840152015460c08201526132e2565b6001600160401b0360405191168152f35b63d2be005d60e01b5f5260045260245ffd5b34611344576020366003190112611344576004356001600160401b03811161134457610693611a6d61069f923690600401611c59565b90612a13565b3461134457604036600319011261134457611a8c611d0c565b336001600160a01b03821603611aa85761154090600435613226565b63334bd91960e11b5f5260045ffd5b34611344575f366003190112611344576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b346113445761069f61069361068e61109461108536611de1565b3461134457604036600319011261134457611540600435611b34611d0c565b90611b416106d0826129e1565b613182565b346113445760203660031901126113445761154060043533612fe1565b346113445760203660031901126113445760206113626004356129e1565b34611344575f366003190112611344576020604051620186a08152f35b346113445761069f610693610e2661068236611d79565b34611344576020366003190112611344576004356001600160401b03811161134457610693611beb61069f923690600401611c59565b90612240565b34611344576020366003190112611344576004359063ffffffff60e01b821680920361134457602091637965db0b60e01b8114908115611c33575b5015158152f35b6301ffc9a760e01b14905083611c2c565b35906001600160e01b03198216820361134457565b9181601f84011215611344578235916001600160401b038311611344576020808501948460051b01011161134457565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b602081016020825282518091526040820191602060408360051b8301019401925f915b838310611cdf57505050505090565b9091929394602080611cfd600193603f198682030187528951611c89565b97019301930191939290611cd0565b602435906001600160a01b038216820361134457565b600435906001600160a01b038216820361134457565b35906001600160a01b038216820361134457565b9181601f84011215611344578235916001600160401b038311611344576020838186019501011161134457565b6080600319820112611344576004356001600160a01b03811681036113445791602435916044356001600160401b0381116113445781611dbb91600401611d4c565b92909291606435906001600160401b03821161134457611ddd91600401611c59565b9091565b60a0600319820112611344576004356001600160a01b03811681036113445791602435916044356001600160401b0381116113445781611e2391600401611d4c565b929092916064356001600160401b0381116113445781611e4591600401611c59565b92909291608435906001600160401b03821161134457611ddd91600401611c59565b60e081019081106001600160401b03821117611e8257604052565b634e487b7160e01b5f52604160045260245ffd5b606081019081106001600160401b03821117611e8257604052565b604081019081106001600160401b03821117611e8257604052565b90601f801991011681019081106001600160401b03821117611e8257604052565b6001600160401b038111611e8257601f01601f191660200190565b929192611f1482611eed565b91611f226040519384611ecc565b829481845281830111611344578281602093845f960137010152565b9080601f8301121561134457816020611f5993359101611f08565b90565b6040600319820112611344576004356001600160401b0381116113445781611f8691600401611c59565b92909291602435906001600160401b03821161134457611ddd91600401611c59565b906040600319830112611344576004356001600160401b0381116113445761016081840360031901126113445760040191602435906001600160401b03821161134457611ddd91600401611d4c565b91908110156120195760051b81013590607e1981360301821215611344570190565b634e487b7160e01b5f52603260045260245ffd5b903590601e198136030182121561134457018035906001600160401b03821161134457602001918160051b3603831361134457565b9190820180921161190057565b6001600160401b038111611e825760051b60200190565b9035601e19823603018112156113445701602081359101916001600160401b038211611344578160051b3603831361134457565b9035603e1982360301811215611344570190565b9060038210156120db5752565b634e487b7160e01b5f52602160045260245ffd5b9035601e19823603018112156113445701602081359101916001600160401b03821161134457813603831361134457565b908060209392818452848401375f828201840152601f01601f1916010190565b9081359160038310156113445761216a60409161216084611f59966120ce565b60208101906120ef565b9190928160208201520191612120565b35906001600160601b038216820361134457565b6020906001600160601b03906121ba9083906001600160a01b036121b182611d38565b1686520161217a565b16910152565b600211156120db57565b80358252602081013591600283101561134457826121ea611f59946121c0565b602082015261221e61221361220260408501856120ef565b608060408601526080850191612120565b9260608101906120ef565b916060818503910152612120565b9035607e1982360301811215611344570190565b90915f925f5b8181106129ae57506122578461206f565b936122656040519586611ecc565b808552612274601f199161206f565b015f5b81811061299b57505083925f945f60018060a01b037f000000000000000000000000000000000000000000000000000000000000000016935b8082106122c1575050505050909150565b6122cc828286611ff7565b97602089016122db818b61202d565b809b9150156129895761ffff8b11612970578a6122f8828061202d565b90500361294e576123269a5061230e818061202d565b93906123198561206f565b946040519d8e9687611ecc565b80865260206123348261206f565b960195601f19013687375f5b8181106127c357505050883b15611344576040519063e20e5d9f60e01b82526040600483015260c482016123748480612086565b8092608060448701525260e4840160e48360051b86010192825f60fe19823603015b838210612724575050505050506123ad8585612086565b604319858403016064860152808352602083019060208160051b85010193835f905b8382106126ef5750505050505061240a906123f885969798999a9b9c9d9e9f95604001876120ef565b85830360431901608487015290612120565b60608501969083908d906001600160a01b036124258b611d38565b1660a484015260031983820301602484015260208751918281520193905f905b8082106126d15750505081805f9403915afa91821561189157612472926126c1575b5094939294936129ff565b9061247d838661202d565b9290505f955b8387106124a357505050505060019150925b0190969594939291966122b0565b9091929394866124bd816124b7898661202d565b90611ff7565b6124d1826124cb868061202d565b90612e5f565b90838d6124f76124ef896124e78735988d612f00565b518887614545565b939092612f00565b5215806126a4575b612520575b5050505f19811461190057600196870196019493929190612483565b602081013560028110156113445760019061253a816121c0565b036126955761254c6040820182612f14565b5091604083013583016060612563604084016129ff565b920135926001600160601b03841680940361134457806060612586920190612f14565b9390925a603f810290808204603f149015171561190057829060061c10612686576001600160a01b031694853b15611344575f8660209261260d83976125fb996040519a8b998a98899663a12da43f60e01b885201356004870152606060248701526064860190604060208201359101612120565b84810360031901604486015291612120565b0393f19081612676575b5061266f577f5c5960582bfc7a494183b4e9a66bfe8ecffc07a83a48d136e732400f7b98bf5090612646612fb2565b906126636040519283928352604060208401526040830190611c89565b0390a25b5f8080612504565b5050612667565b5f61268091611ecc565b5f612617565b6307099c5360e21b5f5260045ffd5b63b90a25b160e01b5f5260045ffd5b506001600160a01b036126b9604084016129ff565b1615156124ff565b5f6126cb91611ecc565b5f612467565b92509250926020806001928651815201940192019185928f92612445565b909192939495602080612716600193601f19888203018a526127118b8761222c565b6121ca565b9801960194939201906123cf565b90919293949560e3198982030186528635908282121561134457602080918660019401908135815260e08061277061275e868601866120ba565b61010087860152610100850190612140565b93612781604085016040830161218e565b63ffffffff821b61279460808301611c44565b16608085015260a081013560a085015260c081013560c085015201359101529801960192019093929193612396565b6127ce818385612e5f565b9061010082360312611344578f6040516127e781611e67565b833581526020840135936001600160401b0385116113445761292c61294792859261291c61281a60019936908401612e81565b602084019081526128c56128313660408601612ecf565b80604087015261284360808601611c44565b60608701908152608087019360a087013585526128d361288261287b60a08b019560c08b0135875260e060c08d019b01358b5261498d565b92516149d9565b916128c561288e6143f4565b945160408051602081019788529081019390935260608301949094526001600160e01b0319909316608082015291829060a0820190565b03601f198101835282611ecc565b519020946128df61445d565b96519351915190519160405196879560208701998a9260a094919796959260c0850198855260208501526040840152606083015260808201520152565b519020612927614827565b6148d6565b926129428461293c848a8c612e5f565b35614504565b612f00565b5201612340565b612959818c9261202d565b90506377e4aa5360e11b5f5260045260245260445ffd5b8a6377e4aa5360e11b5f5260045261ffff60245260445ffd5b50509293949596975090600190612495565b6060602082880181019190915201612277565b936129d76001916129cf6129c58886899899611ff7565b602081019061202d565b919050612062565b9401929192612246565b5f525f80516020615247833981519152602052600160405f20015490565b356001600160a01b03811681036113445790565b919091612a208382612240565b925f5b818110612a2f57505050565b80612a486060612a426001948688611ff7565b016129ff565b828060a01b0381165f52826020526001600160601b0360405f20541680612a72575b505001612a23565b612a7b91612fe1565b5f80612a6a565b601f1981019190821161190057565b9190820391821161190057565b906001600160601b03809116911601906001600160601b03821161190057565b80546bffffffffffffffffffffffff60601b191660609290921b6bffffffffffffffffffffffff60601b16919091179055565b90816020910312611344575180151581036113445790565b9360c095919897969360ff9360e087019a60018060a01b0316875260018060a01b031660208701526040860152606085015216608083015260a08201520152565b35906001600160401b038216820361134457565b359063ffffffff8216820361134457565b91908260e091031261134457604051612b8781611e67565b60c08082948035845260208101356020850152612ba660408201612b4a565b6040850152612bb760608201612b5e565b6060850152612bc860808201612b5e565b6080850152612bd960a08201612b5e565b60a08501520135910152565b91612bfe91833560201c6001600160a01b0316846137db565b50906040612c3d611778612c2d612c1485613bfc565b90506001600160401b0342911610946080369101612b6f565b6001600160401b03421690613c98565b6001600160601b03825191612c5183611e96565b60018352602083018590521691018190526001607f1b9115612c79576001607e1b5b1717905d565b5f612c73565b612c8b612ca891613304565b6001600160a01b039091165f90815260016020526040902061334d565b5090565b612cb5346133fb565b335f5260016020526001600160601b03612cd660405f209282845416612a9e565b166001600160601b03198254161790556040513481527fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c60203392a2565b9081358152612d9b612d29602084018461222c565b6101606020840152612d3f61016084018261218e565b612d62612d4f60408301836120ba565b60806101a08601526101e0850190612140565b906001600160e01b031990612d7990606001611c44565b166101c0840152612d8d60408501856120ef565b908483036040860152612120565b612da860608401846120ba565b8282036060840152803560028110156113445761014092604061216a859484612dd3612ddf966121c0565b845260208101906120ef565b936080810135608085015260a081013560a08501526001600160401b03612e0860c08301612b4a565b1660c085015263ffffffff612e1f60e08301612b5e565b1660e085015263ffffffff612e376101008301612b5e565b1661010085015263ffffffff612e506101208301612b5e565b16610120850152013591015290565b91908110156120195760051b8101359060fe1981360301821215611344570190565b91906040838203126113445760405190612e9a82611eb1565b8193803560038110156113445783526020810135916001600160401b03831161134457602092612eca9201611f3e565b910152565b919082604091031261134457604051612ee781611eb1565b6020612eca818395612ef881611d38565b85520161217a565b80518210156120195760209160051b010190565b903590601e198136030182121561134457018035906001600160401b0382116113445760200191813603831361134457565b604090611f59949281528160208201520191612120565b919290916001600160a01b0316803b1561134457612f95935f809460405196879586948593636691f64760e01b855260048501612f46565b03925af1801561189157612fa65750565b5f612fb091611ecc565b565b3d15612fdc573d90612fc382611eed565b91612fd16040519384611ecc565b82523d5f602084013e565b606090565b9060018060a01b03821691825f5260016020526001600160601b0360405f2054166001600160601b03613013846133fb565b16116130a0575f8080848194613028826133fb565b88845260016020526001600160601b03806040862092818454160316166001600160601b03198254161790555af161305e612fb2565b50156130915760207f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b6591604051908152a2565b6312171d8360e31b5f5260045ffd5b8263112fed8b60e31b5f5260045260245ffd5b5f8181525f805160206152478339815191526020908152604080832033845290915290205460ff16156130e35750565b63e2517d3f60e01b5f523360045260245260445ffd5b6001600160a01b0381165f9081525f80516020615207833981519152602052604090205460ff1661317d576001600160a01b03165f8181525f8051602061520783398151915260205260408120805460ff191660011790553391907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d8180a4600190565b505f90565b5f8181525f80516020615247833981519152602090815260408083206001600160a01b038616845290915290205460ff16613220575f8181525f80516020615247833981519152602090815260408083206001600160a01b0395909516808452949091528120805460ff19166001179055339291907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d9080a4600190565b50505f90565b5f8181525f80516020615247833981519152602090815260408083206001600160a01b038616845290915290205460ff1615613220575f8181525f80516020615247833981519152602090815260408083206001600160a01b0395909516808452949091528120805460ff19169055339291907ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9080a4600190565b906001600160401b03809116911601906001600160401b03821161190057565b611f599062ffffff60406001600160401b0360208401511692015116906132c2565b906001600160c11b0319821661332c57602082901c6001600160a01b03169163ffffffff1690565b6341abc80160e01b5f5260045ffd5b63020000008210156120195701905f90565b63ffffffff82169190602083101561339f576401fffffffe905460c01c9160011b169180830460021490151715611900576001600160401b03906003831b1616901c9060026001831615159216151590565b916133aa9150612a82565b908160011b91808304600214811517156119005760ff916133da9160071c6001600160f81b03169060010161333b565b90549060031b1c9116906003821b16901c9060026001831615159216151590565b6001600160601b038111613415576001600160601b031690565b6306dfcc6560e41b5f52606060045260245260445ffd5b6040516323b872dd60e01b81526001600160a01b039182166004820152306024820152604481018490529192917f0000000000000000000000000000000000000000000000000000000000000000909116906020905f9060649082855af19081601f3d1160015f5114161516613544575b5015613508576020816134ff6134d37ff645c19720906ca336d36d26058a9489c6c757fe35843b75a74e3b8aa972ecf5946133fb565b9460018060a01b031694855f52600184526117c860405f20916001600160601b03835460601c16612a9e565b604051908152a2565b60405162461bcd60e51b81526020600482015260146024820152731514905394d1915497d19493d357d1905253115160621b6044820152606490fd5b3b153d171590505f61349d565b91908110156120195760051b81013590603e1981360301821215611344570190565b5f905b82821061358257505050565b909192613599613593848685613551565b8061202d565b9390946135aa6129c5838387613551565b939094868503613612575f5b878110156135ff578060051b90818a01359161015e198b3603018312156113445787821015612019576001926135f16135f9928b018b612f14565b918d01612be5565b016135b6565b5095509550925060019150019091613576565b86856377e4aa5360e11b5f5260045260245260445ffd5b90600182811c92168015613657575b602083101461364357565b634e487b7160e01b5f52602260045260245ffd5b91607f1691613638565b604051905f825f805160206151a7833981519152549161368083613629565b808352926001811690811561370f57506001146136a4575b612fb092500383611ecc565b505f805160206151a78339815191525f90815290917f42ad5d3e1f2e6e70edcf6d991b8a3023d3fca8047a131592f9edb9fd9b89d57d5b8183106136f3575050906020612fb092820101613698565b60209193508060019154838589010152019101909184926136db565b60209250612fb094915060ff191682840152151560051b820101613698565b604051905f825f805160206151c7833981519152549161374d83613629565b808352926001811690811561370f575060011461377057612fb092500383611ecc565b505f805160206151c78339815191525f90815290917f5f9ce34815f8e11431c7bb75a8e6886a91478f7ffc1dbb0a98dc240fddd76b755b8183106137bf575050906020612fb092820101613698565b60209193508060019154838589010152019101909184926137a7565b91939290610160833603126113445760405160a081018181106001600160401b03821117611e825760405283359384825260208101356001600160401b03811161134457810190608082360312611344576040519161383983611e96565b6138433682612ecf565b835260408101356001600160401b038111611344576138769161386b60609236908301612e81565b602086015201611c44565b60408301526020830191825260408101356001600160401b03811161134457810136601f82011215611344576138b3903690602081359101611f08565b916040840192835260608201356001600160401b038111611344578201604081360312611344576040516138e681611eb1565b813560028110156113445781526020820135916001600160401b03831161134457613af89461391e613934926128c595369101611f3e565b6020840152606088019283526080369101612b6f565b6080870190815261394361445d565b9651935161394f6143f4565b906139a261395d825161498d565b6128c561396d60208501516149d9565b6040948501518551602081019788529586019390935260608501526001600160e01b03199091166080840152829060a0820190565b51902095516020815191012091516139b86141b0565b602081519101209060208151916139ce836121c0565b01516020815191012060405191602083019384526139eb816121c0565b6040830152606082015260608152613a04608082611ecc565b5190209051613a116141f5565b604051613a3d6020828180820195805191829101875e81015f838201520301601f198101835282611ecc565b519020908051906020810151906001600160401b0360408201511663ffffffff60608301511663ffffffff6080840151169160c063ffffffff60a08601511694015194604051966020880198895260408801526060870152608086015260a085015260c084015260e08301526101008201526101008152613ac061012082611ecc565b5190209160405196879560208701998a9260a094919796959260c0850198855260208501526040840152606083015260808201520152565b51902094613b0886612927614827565b93600160c01b1615613bc55791602091613b3993604051809581948293630b135d3f60e11b84528960048501612f46565b03916001600160a01b0316620186a0fa908115611891575f91613b82575b506001600160e01b0319166374eca2c160e11b01613b73579190565b638baa579f60e01b5f5260045ffd5b90506020813d602011613bbd575b81613b9d60209383611ecc565b8101031261134457516001600160e01b031981168103611344575f613b57565b3d9150613b90565b613bd7613bdd91613be6943691611f08565b846148f3565b9093919361492d565b6001600160a01b03908116911603613b73579190565b613c0a906080369101612b6f565b90815160208301511061332c5763ffffffff606083015116608083019063ffffffff8251161061332c5763ffffffff90511660a083019063ffffffff8251161061332c57613c779063ffffffff6001600160401b036040613c6a876148b3565b96015116915116906132c2565b9162ffffff6001600160401b03613c8e8386613d7a565b161161332c579190565b604081016001600160401b0380825116931692831115613d73576001600160401b03613cc3836148b3565b168311613d6c576001600160401b03815116926001600160401b03613cf4606085019563ffffffff875116906132c2565b16811115613d0757505060209150015190565b613d34906001600160401b0363ffffffff613d286020870151875190612a91565b96511693511690612a91565b915191838102938185041490151715611900578015613d5857611f59920490612062565b634e487b7160e01b5f52601260045260245ffd5b5050505f90565b5090505190565b906001600160401b03809116911603906001600160401b03821161190057565b9590929796949360018060a01b031697885f526001602052613dbf8560405f2061334d565b906141525761413e576001600160401b0386169889421161412657613ded611778612c2d3660808c01612b6f565b96815f52600160205260405f20996001600160601b038b5416946001600160601b038a1693848710614114575060018060a01b031698895f52600160205260405f20906001600160601b03825460601c16966101408d013580981061410157918d6001600160601b0380613e9394613e989897960316166001600160601b03198254161790556001600160601b03613e84896133fb565b81835460601c16031690612abe565b613d7a565b926001600160401b03841662ffffff81116140ea5750613eb7906133fb565b60405193613ec485611e67565b88855260208086019c8d5262ffffff90911660408087019182525f60608801818152608089019687526001600160601b0390951660a0808a0191825260c08a019889528e35808452958390529290912097519e51925194519290911b67ffffffffffffffff60a01b166001600160a01b039e909e169d909d1760e09390931b62ffffff60e01b169290921760f89290921b6001600160f81b031916919091178455996001840191516001600160601b03166001600160601b03166001600160601b0319835416178255516001600160601b0316613fa091612abe565b51906002015563ffffffff831692602084105f1461405b576401fffffffe9060011b1692808404600214901517156119005785546001600160c01b038116600190941b6001600160401b031660c091821c17901b6001600160c01b031916929092179094557fe5e43c93dc0ec595ed3b122bdc6d39a480e9d17fb6812e0f90cfc4ba33b0969e93614056915b6140486040519586958652606060208701526060860190612d14565b918483036040860152612120565b0390a2565b509161406690612a82565b918260011b9583870460021484151715611900577fe5e43c93dc0ec595ed3b122bdc6d39a480e9d17fb6812e0f90cfc4ba33b0969e96614056946140e59260ff916001916140c29160071c6001600160f81b031690830161333b565b929093161b82548260031b1c179082549060031b91821b915f19901b1916179055565b61402c565b6306dfcc6560e41b5f52601860045260245260445ffd5b8b63112fed8b60e31b5f5260045260245ffd5b63112fed8b60e31b5f5260045260245ffd5b898863cfe6a8fd60e01b5f523560045260245260445ffd5b86631cfdeebb60e01b5f523560045260245ffd5b8763a905765160e01b5f523560045260245ffd5b60405190614175606083611ecc565b60268252654c696d69742960d01b6040837f43616c6c6261636b286164647265737320616464722c75696e7439362067617360208201520152565b604051906141bf606083611ecc565b60218252602960f81b6040837f496e7075742875696e743820696e707574547970652c6279746573206461746160208201520152565b6040519061420460c083611ecc565b60888252676c61746572616c2960c01b60a0837f4f666665722875696e74323536206d696e50726963652c75696e74323536206d60208201527f617850726963652c75696e7436342072616d70557053746172742c75696e743360408201527f322072616d705570506572696f642c75696e743332206c6f636b54696d656f7560608201527f742c75696e7433322074696d656f75742c75696e74323536206c6f636b436f6c60808201520152565b604051906142c2606083611ecc565b602982526874657320646174612960b81b6040837f5072656469636174652875696e743820707265646963617465547970652c627960208201520152565b6040519061430f608083611ecc565b605a82527f6c2c496e70757420696e7075742c4f66666572206f66666572290000000000006060837f50726f6f66526571756573742875696e743235362069642c526571756972656d60208201527f656e747320726571756972656d656e74732c737472696e6720696d616765557260408201520152565b60405190614396608083611ecc565b60438252626f722960e81b6060837f526571756972656d656e74732843616c6c6261636b2063616c6c6261636b2c5060208201527f7265646963617465207072656469636174652c6279746573342073656c65637460408201520152565b6143fc614387565b6020614457614409614166565b826144126142b3565b8160405195869481808701998051918291018b5e8601908282015f8152815193849201905e0101905f8252805192839101825e015f815203601f198101835282611ecc565b51902090565b614465614300565b61446d614166565b6144756141b0565b9061447e6141f5565b6144866142b3565b61448e614387565b916040519485946020860197805160208192018a5e860160208101915f83528051926020849201905e016020015f815281516020819301825e015f815281516020819301825e015f815281516020819301825e015f815281516020819301825e015f815203601f19810182526144579082611ecc565b9190825f525f60205280600260405f200154146145405761452490614a4a565b5161453c575063c274d3e360e01b5f5260045260245ffd5b9050565b509050565b909391936060935f9461455783613304565b60018060a01b0382165f5260016020526145748160405f2061334d565b9290809460405161458481611e67565b5f81525f60208201525f60408201525f828201525f60808201525f60a08201525f60c0820152916147ad575b506145ba8b614a4a565b8051909590156147385760208601516146c8579286959492888d937fd78a37a26380237bbe8f5a5221dcf308b87fbf79aa163180e0797d675020c88b99965b156146ad5760208101516001600160401b0316421161468d5761461c9750614e24565b965b875161464f575b61464a60405192839283526040602084015260018060a01b03169560408301906121ca565b0390a3565b7f210e4fd706e561df48472433bcc50b4589f2c13e784e9992f4c3e6de26eb35646040516020815280614685602082018c611c89565b0390a1614625565b9291906001600160601b0360406146a79901511693614bcd565b9661461e565b5050906001600160601b0360406146a7970151169189614a94565b5050505050505092505091506040519063873fd26b60e01b60208301526024820152602481526146f9604482611ecc565b7f210e4fd706e561df48472433bcc50b4589f2c13e784e9992f4c3e6de26eb3564604051602081528061472f6020820185611c89565b0390a190600190565b80806147a0575b1561478d5761474d826132e2565b6001600160401b03429116106146c8579286959492888d937fd78a37a26380237bbe8f5a5221dcf308b87fbf79aa163180e0797d675020c88b99966145f9565b8763c274d3e360e01b5f5260045260245ffd5b508b60c08301511461473f565b9050865f525f602052600260405f206001600160601b03604051936147d185611e67565b825460018060a01b03811686526001600160401b038160a01c16602087015262ffffff8160e01c16604087015260f81c8186015260018301549082821660808701521c1660a0840152015460c08201525f6145b0565b61482f614faa565b614837615001565b6040519060208201927f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8452604083015260608201524660808201523060a082015260a0815261445760c082611ecc565b60ff5f805160206152678339815191525460401c16156148a457565b631afcd79f60e31b5f5260045ffd5b611f599063ffffffff60806001600160401b0360408401511692015116906132c2565b6042916040519161190160f01b8352600283015260228201522090565b81519190604183036149235761491c9250602082015190606060408401519301515f1a906150d0565b9192909190565b50505f9160029190565b60048110156120db578061493f575050565b600181036149565763f645eedf60e01b5f5260045ffd5b60028103614971575063fce698f760e01b5f5260045260245ffd5b60031461497b5750565b6335e2f38360e21b5f5260045260245ffd5b614995614166565b60208151910120906001600160601b03602060018060a01b038351169201511660405191602083019384526040830152606082015260608152614457608082611ecc565b6149e16142b3565b602081519101209080519060038210156120db576020015160208151910120614a18604051926020840194855260408401906120ce565b606082015260608152614457608082611ecc565b60405190614a3982611e96565b5f6040838281528260208201520152565b614a52614a2c565b505c614a5c614a2c565b506001600160601b0360405191614a7283611e96565b6001607f1b8116151583526001607e1b81161515602084015216604082015290565b9694959192939096606096614b80575f805160206152a783398151915260209596979860018060a01b031693845f5260018752614ad560405f209687615033565b6040519384526001600160a01b0316958693a36001600160601b03825416906001600160601b0385168210614b5457506001600160601b038481920316166001600160601b03198254161790555f5260016020526001600160601b03614b4260405f209282845416612a9e565b166001600160601b0319825416179055565b949550505050506040519063112fed8b60e31b6020830152602482015260248152611f59604482611ecc565b955050505050915060405190631cfdeebb60e01b6020830152602482015260248152611f59604482611ecc565b906001600160601b03809116911603906001600160601b03821161190057565b9395979692949094606098600160608701511615158015614e14575b614de55715614d97575b50506001600160a01b03165f908152600160205260408120608093909301516001600160601b038681169695929491168581881115614d645781614c3691614bad565b906001600160601b03835416906001600160601b0383168210614d3f575b5082546bffffffffffffffffffffffff19169190036001600160601b03161790555b5f90815260208190526040902080546affffffffffffffffffffff60a01b81166001600160a01b0384169081176001600160a01b0319929092161760f890811c600217901b6001600160f81b03191617905560018060a01b03165f52600160205260405f206001600160601b03614cf08482845416612a9e565b166001600160601b0319825416179055614d08575050565b6001600160601b039192935060405192636008fdcb60e01b6020850152602484015216604482015260448152611f59606482611ecc565b96509450506001600160601b0380614d58868098612a9e565b96600196915091614c54565b614d79614d82916001600160601b0393614bad565b82845416612a9e565b166001600160601b0319825416179055614c76565b6001600160a01b0383165f908152600160205260409020614db89190615033565b6040519081526001600160a01b0383169085905f805160206152a783398151915290602090a35f80614bf3565b5050505050509192505060405190631cfdeebb60e01b6020830152602482015260248152611f59604482611ecc565b5060026060870151161515614be9565b9391909296959496606097600160608701511615158015614f9a575b614f6c5715614f23575b505082516001600160a01b039485169416841480159190614f14575b50614eea5760a0612fb093926001600160601b03925f525f6020525f6001604082208160f81b828060f81b03825416178155015582608082015116845f52600160205283614ebb60405f209282845416612a9e565b168419825416179055015116905f5260016020526117c860405f20916001600160601b03835460601c16612a9e565b92935050506040519063a905765160e01b6020830152602482015260248152611f59604482611ecc565b905060c083015114155f614e66565b614f3f9160018060a01b03165f52600160205260405f20615033565b6040518181526001600160a01b0385169083905f805160206152a783398151915290602090a35f80614e4a565b50505050929350505060405190631cfdeebb60e01b6020830152602482015260248152611f59604482611ecc565b5060026060870151161515614e40565b614fb2613661565b8051908115614fc2576020012090565b50505f805160206151e7833981519152548015614fdc5790565b507fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a47090565b61500961372e565b8051908115615019576020012090565b50505f80516020615287833981519152548015614fdc5790565b9063ffffffff8116906020821015615090576401fffffffe9060011b1690808204600214901517156119005781546001600160c01b038116600290921b6001600160401b031660c091821c17901b6001600160c01b031916179055565b5061509a90612a82565b8060011b908082046002148115171561190057612fb09260ff916002916140c29160071c6001600160f81b03169060010161333b565b91906fa2a8918ca85bafe22016d0b997e4df60600160ff1b03841161513d579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15611891575f516001600160a01b0381161561513357905f905f90565b505f906001905f90565b5050505f9160039190565b9061516c575080511561515d57602081519101fd5b63d6bda27560e01b5f5260045ffd5b8151158061519d575b61517d575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b1561517556fea16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d102a16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d103a16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100b7db2dd08fcb62d0c9e08c51941cae53c267786a0b75803fb7960902fc8ef97d360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00a16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d101120ea8d7610aa46e4a31b254c5d07489ebe8f1a93dc7bbbe60eaf3db2c62c0cca164736f6c634300081a000a")] + #[sol(rpc, bytecode = "610100346101f357601f6158a638819003918201601f19168301916001600160401b038311848410176101f7578084926060946040528339810103126101f35780516001600160a01b03811691908281036101f35761006c60406100656020850161020b565b930161020b565b9230608052156101e4576001600160a01b038216156101d5576001600160a01b038316156101c65760a05260c05260e0527ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005460ff8160401c166101b7576002600160401b03196001600160401b0382160161014e575b60405161568690816102208239608051818181610d540152610e7c015260a0518181816106f401526121bb015260c051818181610a3d01528181610f3f01528181611119015281816119e901528181611a9201526133d2015260e05181818161149301526141540152f35b6001600160401b0319166001600160401b039081177ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290602090a15f6100e3565b63f92ee8a960e01b5f5260045ffd5b6307c71f2360e11b5f5260045ffd5b633a001e0560e11b5f5260045ffd5b63466d7fef60e01b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b51906001600160a01b03821682036101f35756fe60806040526004361061414a575f3560e01c806301ffc9a714610331578063122bf1181461032c5780631472e479146103275780631ce0302414610322578063248a9ca31461031d5780632e1a7d4d146103185780632f2ff15d14610313578063329264ab1461030e57806332fe7b261461030957806336568abe146103045780633f3e2c0d146102ff57806341451f94146102fa57806345bc4d10146102f55780634cefb7cf146102f05780634f1ef286146102eb57806352d1902d146102e6578063553c0248146102a05780635b07fdd8146102e15780635d704b33146102dc57806360dfd4a9146102d75780636112fe2e146102d2578063672b0194146102cd57806370a08231146102c857806375b238fc146102a057806379965fdf146102c357806381bf6c24146102be57806384b0196e146102b957806391d14854146102b4578063956b0960146102af578063989fff14146102aa5780639c7a8c61146102a5578063a217fddf146102a0578063ad3cb1cc1461029b578063ae7330f114610296578063b09c980b14610291578063b760faf91461028c578063bad4a01f14610287578063c4d66de814610282578063c515c15f1461027d578063c64067a214610278578063cb74db1114610273578063d0e30db01461026e578063d547741f14610269578063dbfb7e7e14610264578063df2e67061461025f578063eba2ecc81461025a578063ef1ae1c814610255578063f2800f1a14610250578063fd737ea81461024b578063ff1214a5146102465763ffa1ad740361414a57611cc8565b611b13565b611a5b565b611a18565b6119d4565b611997565b61192d565b611916565b6118e2565b6118cf565b6118a7565b611890565b6117a0565b61164a565b61162c565b6115b2565b61156b565b611520565b6114d9565b610ec1565b6114c2565b61147e565b611462565b611404565b61135a565b61128e565b61126e565b6111de565b6111c4565b611068565b610fc4565b610f15565b610edb565b610e6a565b610d12565b610bd0565b610895565b610785565b61076b565b610723565b6106df565b6106ac565b6105f4565b6105d5565b6105af565b610592565b610560565b610490565b610359565b6001600160e01b031981160361034857565b5f80fd5b359061035782610336565b565b3461034857602036600319011261034857602060043561037881610336565b63ffffffff60e01b16637965db0b60e01b811490811561039e575b506040519015158152f35b6301ffc9a760e01b1490505f610393565b9181601f84011215610348578235916001600160401b038311610348576020808501948460051b01011161034857565b602060031982011261034857600435906001600160401b03821161034857610409916004016103af565b9091565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b602081016020825282518091526040820191602060408360051b8301019401925f915b83831061046357505050505090565b9091929394602080610481600193603f19868203018752895161040d565b97019301930191939290610454565b34610348576104b66104aa6104a4366103df565b906121a2565b60405191829182610431565b0390f35b6001600160a01b0381160361034857565b3590610357826104ba565b9181601f84011215610348578235916001600160401b038311610348576020838186019501011161034857565b60806003198201126103485760043561051b816104ba565b91602435916044356001600160401b038111610348578161053e916004016104d6565b92909291606435906001600160401b03821161034857610409916004016103af565b34610348576104b66104aa61058361057736610503565b95939094929192612e7d565b61236b565b5f91031261034857565b34610348575f366003190112610348576020604051620186a08152f35b346103485760203660031901126103485760206105cd60043561232a565b604051908152f35b34610348576020366003190112610348576105f260043533612eff565b005b34610348576040366003190112610348576105f2602435600435610617826104ba565b6106286106238261232a565b613005565b6130d4565b60a060031982011261034857600435610645816104ba565b91602435916044356001600160401b0381116103485781610668916004016104d6565b929092916064356001600160401b038111610348578161068a916004016103af565b92909291608435906001600160401b03821161034857610409916004016103af565b34610348576104b66104aa6106da6106d56106c63661062d565b98969793929491959097612e7d565b613518565b6121a2565b34610348575f366003190112610348576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b3461034857604036600319011261034857600435602435610743816104ba565b336001600160a01b0382160361075c576105f29161317c565b63334bd91960e11b5f5260045ffd5b34610348576104b66104aa61077f366103df565b9061236b565b34610348576020366003190112610348576004356107a281612909565b15610883575f525f6020526104b661086960405f206002604051916107c683610c0e565b80546001600160a01b038116845260a081901c6001600160401b0316602085015261081090610806905b62ffffff60e082901c1660408701525b60f81c90565b60ff166060850152565b61085d61084d600183015461083e61082e826001600160601b031690565b6001600160601b03166080880152565b60601c6001600160601b031690565b6001600160601b031660a0850152565b015460c082015261323c565b6040516001600160401b0390911681529081906020820190565b63d2be005d60e01b5f5260045260245ffd5b34610348576020366003190112610348576004356108c56108b58261325e565b6108c0829392612352565b6132a7565b5015610bbc576108e46108df835f525f60205260405f2090565b6123dc565b6060810151600416610ba8576060810151600116610b94576109146109088261323c565b6001600160401b031690565b421115610b635761095b61092f845f525f60205260405f2090565b80546001600160f81b03811660f891821c60041790911b6001600160f81b0319161781555f9060010155565b6109856109b86109b360a084016109ae61099e61099661099161098585516001600160601b031690565b6001600160601b031690565b61247f565b612710900490565b948592516001600160601b031690565b6124de565b613375565b82519092906001600160a01b0316936109d8826060600291015116151590565b15610afa575050610a0f6109eb84612352565b610a0984610a0483546001600160601b039060601c1690565b6124eb565b9061250b565b60405163a9059cbb60e01b815261dead600482015260248101829052926020846044815f6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000165af1908115610af5577f79ca7c80cf57b513ffdf8aa37ec70e40757f5e0d35219241860bb4b4c2fa761694610ac392610ac8575b50604080519384526001600160601b0390941660208401526001600160a01b0316928201929092529081906060820190565b0390a2005b610ae99060203d602011610aee575b610ae18183610c64565b810190612559565b610a91565b503d610ad7565b612197565b610b5e919450610b58610b46610b4060803098610b32610b1930612352565b610a098b610a0483546001600160601b039060601c1690565b01516001600160601b031690565b92612352565b91610a0483546001600160601b031690565b9061253e565b610a0f565b82610b70610b919261323c565b63079c66ab60e41b5f526004919091526001600160401b0316602452604490565b5ffd5b631cfdeebb60e01b5f52600483905260245ffd5b633231064d60e11b5f52600483905260245ffd5b63d2be005d60e01b5f52600482905260245ffd5b34610348576040366003190112610348576105f2600435610bf0816104ba565b60243590336133a6565b634e487b7160e01b5f52604160045260245ffd5b60e081019081106001600160401b03821117610c2957604052565b610bfa565b606081019081106001600160401b03821117610c2957604052565b604081019081106001600160401b03821117610c2957604052565b90601f801991011681019081106001600160401b03821117610c2957604052565b6040519061035760e083610c64565b6040519061035760a083610c64565b6001600160401b038111610c2957601f01601f191660200190565b929192610cca82610ca3565b91610cd86040519384610c64565b829481845281830111610348578281602093845f960137010152565b9080601f8301121561034857816020610d0f93359101610cbe565b90565b604036600319011261034857600435610d2a816104ba565b6024356001600160401b03811161034857610d49903690600401610cf4565b906001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016308114908115610e48575b50610e3957610d8c612fc9565b6040516352d1902d60e01b8152916020836004816001600160a01b0386165afa5f9381610e08575b50610dd557634c9c8ce360e01b5f526001600160a01b03821660045260245ffd5b905f805160206155da8339815191528303610df4576105f292506146e0565b632a87526960e21b5f52600483905260245ffd5b610e2b91945060203d602011610e32575b610e238183610c64565b8101906134d0565b925f610db4565b503d610e19565b63703e46dd60e11b5f5260045ffd5b5f805160206155da833981519152546001600160a01b0316141590505f610d7f565b34610348575f366003190112610348577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03163003610e395760206040515f805160206155da8339815191528152f35b34610348575f3660031901126103485760206040515f8152f35b34610348575f3660031901126103485760206105cd61477f565b6044359060ff8216820361034857565b6064359060ff8216820361034857565b34610348575f60a036600319011261034857600435602435610f35610ef5565b90606435608435927f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690813b15610348575f8094610f956040519788968795869463d505accf60e01b86528c303360048901612571565b03925af1610fad575b50610faa9033336133a6565b80f35b610fba9192505f90610c64565b5f90610faa610f9e565b34610348576020366003190112610348576004355f525f6020526104b661105660405f20600260405191610ff783610c0e565b80546001600160a01b038116845260a081901c6001600160401b0316602085015261102590610806906107f0565b61104361084d600183015461083e61082e826001600160601b031690565b015460c082015260600151600416151590565b60405190151581529081906020820190565b346103485760203660031901126103485760043561109861108833612352565b5460601c6001600160601b031690565b6001600160601b036110ac61098584613375565b9116106111b1576110ee6110bf82613375565b610a096110cb33612352565b916110e183546001600160601b039060601c1690565b036001600160601b031690565b60405163a9059cbb60e01b8152336004820152602481018290526020816044815f6001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000165af1908115610af5575f91611192575b50156111835760405190815233907fa315121c7f539fd811176ad2735d5d3981237b261889ec13ae4d617ad06e39bc908060208101610ac3565b6312171d8360e31b5f5260045ffd5b6111ab915060203d602011610aee57610ae18183610c64565b5f611149565b63112fed8b60e31b5f523360045260245ffd5b34610348576104b66104aa6105836106d56106c63661062d565b34610348576020366003190112610348576004356111fb816104ba565b60018060a01b03165f52600160205260206001600160601b0360405f205416604051908152f35b6040600319820112610348576004356001600160401b038111610348578161124c916004016103af565b92909291602435906001600160401b03821161034857610409916004016103af565b34610348576104b66104aa6106da61128536611222565b93919092613518565b346103485760203660031901126103485760206112cb6112af60043561325e565b6001600160a01b039091165f90815260018452604090206132a7565b90506040519015158152f35b9293916112f961130792600f60f81b865260e0602087015260e086019061040d565b90848203604086015261040d565b92606083015260018060a01b031660808201525f60a082015260c0818303910152602080835192838152019201905f5b8181106113445750505090565b8251845260209384019390920191600101611337565b34610348575f366003190112610348575f8051602061559a8339815191525415806113ee575b156113b15761138d6135ed565b6113956136ba565b906104b66113a16125b2565b60405193849330914691866112d7565b60405162461bcd60e51b81526020600482015260156024820152741152540dcc4c8e88155b9a5b9a5d1a585b1a5e9959605a1b6044820152606490fd5b505f8051602061563a8339815191525415611380565b3461034857604036600319011261034857602060ff61145660243560043561142b826104ba565b5f525f805160206155fa833981519152845260405f209060018060a01b03165f5260205260405f2090565b54166040519015158152f35b34610348575f3660031901126103485760206040516113888152f35b34610348575f366003190112610348576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b34610348576104b66104aa61058361128536611222565b34610348575f366003190112610348576104b66040516114fa604082610c64565b60058152640352e302e360dc1b602082015260405191829160208352602083019061040d565b346103485760603660031901126103485760043561153d816104ba565b602435604435916001600160401b038311610348576115636105f29336906004016104d6565b929091612e7d565b3461034857602036600319011261034857600435611588816104ba565b60018060a01b03165f52600160205260206001600160601b0360405f205460601c16604051908152f35b6020366003190112610348576004356115ca816104ba565b6116006115d634613375565b9160018060a01b031691825f526001602052610b5860405f20916001600160601b038354166124eb565b7fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c6020604051348152a2005b34610348576020366003190112610348576105f260043533336133a6565b3461034857602036600319011261034857600435611667816104ba565b5f8051602061561a83398151915254906001600160401b0361169860ff604085901c1615936001600160401b031690565b168015908161178b575b6001149081611781575b159081611778575b50611769576116f790826116ee60016001600160401b03195f8051602061561a8339815191525416175f8051602061561a83398151915255565b611745576125cd565b6116fd57005b5f8051602061561a833981519152805460ff60401b19169055604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290602090a1005b5f8051602061561a833981519152805460ff60401b1916600160401b1790556125cd565b63f92ee8a960e01b5f5260045ffd5b9050155f6116b4565b303b1591506116ac565b8391506116a2565b5f525f60205260405f2090565b34610348576020366003190112610348576004355f90815260208181526040918290208054600182015460029092015484516001600160a01b038316815260a083811c6001600160401b03169582019590955260e083811c62ffffff169682019690965260f89290921c6060808401919091526001600160601b03808516608085015293901c9092169281019290925260c0820152f35b90816101609103126103485790565b906040600319830112610348576004356001600160401b038111610348578261187191600401611837565b91602435906001600160401b03821161034857610409916004016104d6565b34610348576105f26118a136611846565b91612848565b346103485760203660031901126103485760206118c5600435612909565b6040519015158152f35b5f366003190112610348576105f2612936565b34610348576040366003190112610348576105f2602435600435611905826104ba565b6119116106238261232a565b61317c565b34610348576104b66104aa6106da61057736610503565b610ac37fc354af001adff0e8c35481c5ce3df3edee370c71572514d281e884c8cb55220361197c61195d36611846565b94903461198a575b823595604051948594604086526040860190612a32565b918483036020860152611e85565b611992612936565b611965565b34610348576105f26119a836611846565b916119b3813561325e565b906119c085858386613899565b506119ca846139b6565b9690953395613c3d565b34610348575f366003190112610348576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b3461034857602036600319011261034857600435611a3581612909565b15610883575f525f60205260206001600160401b0360405f205460a01c16604051908152f35b34610348575f60c03660031901126103485760043590611a7a826104ba565b602435604435611a88610f05565b9060843560a435927f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690813b15610348575f8094611ae86040519788968795869463d505accf60e01b86528c303360048901612571565b03925af1611afd575b50610faa9192336133a6565b610faa92505f611b0c91610c64565b5f91611af1565b34610348576060366003190112610348576004356001600160401b03811161034857611b43903690600401611837565b6024356001600160401b03811161034857611b629036906004016104d6565b916044356001600160401b03811161034857611b829036906004016104d6565b611b8c833561325e565b91611b9987878488613899565b604051919591611baa606082610c64565b602181527f4c6f636b526571756573742850726f6f665265717565737420726571756573746020820152602960f81b6040820152611be6613e88565b611bee613ed2565b90611bf7613f17565b611bff613fd5565b611c07614022565b90611c106140a9565b92604051958695602087019889611c2691614116565b611c2f91614116565b611c3891614116565b611c4191614116565b611c4a91614116565b611c5391614116565b611c5c91614116565b03601f1981018252611c6e9082610c64565b519020604080516020810192835280820193909352825290611c91606082610c64565b519020611c9d90614128565b913690611ca992610cbe565b611cb291614134565b92611cbc856139b6565b966105f2989196613c3d565b34610348575f36600319011261034857602060405160018152f35b634e487b7160e01b5f52603260045260245ffd5b9190811015611d195760051b81013590607e1981360301821215610348570190565b611ce3565b903590601e198136030182121561034857018035906001600160401b03821161034857602001918160051b3603831361034857565b634e487b7160e01b5f52601160045260245ffd5b91908201809211611d7457565b611d53565b6001600160401b038111610c295760051b60200190565b90611d9a82611d79565b611da76040519182610c64565b8281528092611db8601f1991611d79565b01905f5b828110611dc857505050565b806060602080938501015201611dbc565b9035601e19823603018112156103485701602081359101916001600160401b038211610348578160051b3603831361034857565b9035603e1982360301811215610348570190565b3590600382101561034857565b634e487b7160e01b5f52602160045260245ffd5b906003821015611e4f5752565b611e2e565b9035601e19823603018112156103485701602081359101916001600160401b03821161034857813603831361034857565b908060209392818452848401375f828201840152601f01601f1916010190565b906040611ecb610d0f93611ec184611ebc83611e21565b611e42565b6020810190611e54565b9190928160208201520191611e85565b6001600160601b0381160361034857565b6001600160601b03602080928035611f03816104ba565b6001600160a01b031685520135611f1981611edb565b16910152565b6002111561034857565b60021115611e4f57565b610d0f91813581526020820135611f4981611f1f565b611f5281611f29565b6020820152611f86611f7b611f6a6040850185611e54565b608060408601526080850191611e85565b926060810190611e54565b916060818503910152611e85565b9035607e1982360301811215610348570190565b90602083828152019260208260051b82010193835f925b848410611fcf5750505050505090565b909192939495602080611ff6600193601f19868203018852611ff18b88611f94565b611f33565b9801940194019294939190611fbf565b90602080835192838152019201905f5b8181106120235750505090565b8251845260209384019390920191600101612016565b92916040845260c084019361204e8380611dd9565b809196608060408501525260e082019060e08160051b8401019680925f9060fe1983360301905b8483106120f6575050505050506120e96120d960606120d26120b3610d0f98996120a260208a018a611dd9565b888303603f1901868a015290611fa8565b6120c06040890189611e54565b878303603f1901608089015290611e85565b95016104cb565b6001600160a01b031660a0830152565b6020818403910152612006565b90919293949960df198782030182528a35908382121561034857602080918760019401908135815260e08061214261213086860186611e0d565b61010087860152610100850190611ea5565b936121536040850160408301611eec565b608081013561216181610336565b63ffffffff831b16608085015260a081013560a085015260c081013560c085015201359101529c01920193019190949392612075565b6040513d5f823e3d90fd5b91905f805b8281106122f757506121b890611d90565b927f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316915f90815b8183106121f6575050505050565b612201838386611cf7565b9061220f6020830183611d1e565b809150156122ec5761ffff81116122d4578061222b8480611d1e565b9050036122b057506122466122408380611d1e565b90612ba0565b90863b156103485760405163e20e5d9f60e01b8152915f838061226d848860048401612039565b03818b5afa908115610af55760019461228d948c93612296575b50612cf8565b925b01916121e8565b806122a45f6122aa93610c64565b80610588565b5f612287565b610b91906122be8480611d1e565b6377e4aa5360e11b5f5260045250602452604490565b6377e4aa5360e11b5f5260045261ffff60245260445ffd5b50926001915061228f565b9061232060019161231861230e85878a989a611cf7565b6020810190611d1e565b919050611d67565b91019391936121a7565b5f525f805160206155fa833981519152602052600160405f20015490565b35610d0f816104ba565b6001600160a01b03165f90815260016020526040902090565b91909161237883826121a2565b925f5b81811061238757505050565b8060606123976001938587611cf7565b01356123a2816104ba565b828060a01b0381165f52826020526001600160601b0360405f205416806123cc575b50500161237b565b6123d591612eff565b5f806123c4565b906040516123e981610c0e565b82546001600160a01b038116825260a081901c6001600160401b0316602083015260e081901c62ffffff1660408301529092839160c09160029161243a9061243090610800565b60ff166060860152565b612478612468600183015461083e612458826001600160601b031690565b6001600160601b03166080890152565b6001600160601b031660a0860152565b0154910152565b906113888202918083046113881490151715611d7457565b908160011b9180830460021490151715611d7457565b81810292918115918404141715611d7457565b81156124ca570490565b634e487b7160e01b5f52601260045260245ffd5b91908203918211611d7457565b906001600160601b03809116911601906001600160601b038211611d7457565b80546bffffffffffffffffffffffff60601b191660609290921b6bffffffffffffffffffffffff60601b16919091179055565b906001600160601b03166001600160601b0319825416179055565b90816020910312610348575180151581036103485790565b9360c095919897969360ff9360e087019a60018060a01b0316875260018060a01b031660208701526040860152606085015216608083015260a08201520152565b604051906125c1602083610c64565b5f808352366020840137565b906001600160a01b0382161561279e576125e56147e0565b6125ed6147e0565b6040918251926125fd8185610c64565b601084526f12509bdd5b991b195cdcd3585c9ad95d60821b602085015261262681519182610c64565b60018152603160f81b602082015261263c6147e0565b6126446147e0565b83516001600160401b038111610c29576126748161266f5f8051602061555a833981519152546135b5565b61480b565b6020601f82116001146126fc57816126bf93926126ab926126ee97985f926126f1575b50508160011b915f199060031b1c19161790565b5f8051602061555a833981519152556148b6565b6126d45f5f8051602061559a83398151915255565b6126e95f5f8051602061563a83398151915255565b61304b565b50565b015190505f80612697565b5f8051602061555a8339815191525f52601f198216957f42ad5d3e1f2e6e70edcf6d991b8a3023d3fca8047a131592f9edb9fd9b89d57d965f5b81811061278657509660019284926126bf96956126ee999a1061276e575b505050811b015f8051602061555a833981519152556148b6565b01515f1960f88460031b161c191690555f8080612754565b83830151895560019098019760209384019301612736565b63267eaa8160e21b5f5260045ffd5b35906001600160401b038216820361034857565b359063ffffffff8216820361034857565b91908260e0910312610348576040516127ea81610c0e565b60c08082948035845260208101356020850152612809604082016127ad565b604085015261281a606082016127c1565b606085015261282b608082016127c1565b608085015261283c60a082016127c1565b60a08501520135910152565b9161286191833560201c6001600160a01b031684613899565b50906128916109b3612881612875846139b6565b943691506080016127d2565b6001600160401b03421690613a61565b60405161289d81610c2e565b6001815260208101926001600160401b034291161083526001600160601b0360408201921682525115155f14612902576001607f1b915b51156128f3576001607e1b906001600160601b03905b5116911717905d565b6001600160601b035f916128ea565b5f916128d4565b6129156129329161325e565b6001600160a01b039091165f9081526001602052604090206132a7565b5090565b61296261294234613375565b335f526001602052610b5860405f20916001600160601b038354166124eb565b6040513481527fe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c60203392a2565b906040611ecb610d0f9380356129a581611f1f565b6129ae81611f29565b84526020810190611e54565b60c0809180358452602081013560208501526001600160401b036129e0604083016127ad565b16604085015263ffffffff6129f7606083016127c1565b16606085015263ffffffff612a0e608083016127c1565b16608085015263ffffffff612a2560a083016127c1565b1660a08501520135910152565b610d0f9080358352608080612adb612ac1612a506020860186611f94565b6101606020890152612a66610160890182611eec565b6060612a8a612a786040840184611e0d565b866101a08c01526101e08b0190611ea5565b910135612a9681610336565b6001600160e01b0319166101c0890152612ab36040870187611e54565b9089830360408b0152611e85565b612ace6060860186611e0d565b8782036060890152612990565b940191016129ba565b9190811015611d195760051b8101359060fe1981360301821215610348570190565b91906040838203126103485760405190612b1f82610c49565b8193612b2a81611e21565b83526020810135916001600160401b03831161034857602092612b4d9201610cf4565b910152565b919082604091031261034857604051612b6a81610c49565b60208082948035612b7a816104ba565b8452013591612b8883611edb565b0152565b8051821015611d195760209160051b010190565b919091612bac83611d79565b612bb96040519182610c64565b838152601f19612bc885611d79565b0136602083013780935f5b818110612be05750505050565b612beb818386612ae4565b906101008236031261034857612bff610c85565b91803583526020810135906001600160401b0382116103485760019360e0612c7992612c31612c7e9536908301612b06565b6020840152612c433660408301612b52565b6040840152612c546080820161034c565b606084015260a0810135608084015260c081013560a0840152013560c082015261422e565b614128565b612c9381612c8d84878a612ae4565b356142ea565b612c9d8286612b8c565b5201612bd3565b35610d0f81611f1f565b903590601e198136030182121561034857018035906001600160401b0382116103485760200191813603831361034857565b35610d0f81611edb565b5f198114611d745760010190565b9190612d0660608401612348565b906020840193612d168582611d1e565b9490505f955b858710612d2d575050505050505090565b9091929394959796612d4989612d438487611d1e565b90611cf7565b89612d5e81612d588880611d1e565b90612ae4565b91612d7789612d6f8535948b612b8c565b518484614389565b90612d828689612b8c565b521580612e49575b612dab575b505050612d9d600191612cea565b979801959493929190612d1c565b6001612dbd6020839694959601612ca4565b612dc681611f29565b03612e3a57600193612d9d9382612e01612de66040612e33960183612cae565b50906020820135916040810135019060206040830192013590565b92612e2b612e206060612e1960408a97969701612348565b9801612ce0565b916060810190612cae565b96909561460b565b915f612d8f565b63b90a25b160e01b5f5260045ffd5b506001600160a01b03612e5e60408501612348565b161515612d8a565b604090610d0f949281528160208201520191611e85565b919290916001600160a01b0316803b1561034857612eb5935f809460405196879586948593636691f64760e01b855260048501612e66565b03925af18015610af557612ec65750565b5f61035791610c64565b3d15612efa573d90612ee182610ca3565b91612eef6040519384610c64565b82523d5f602084013e565b606090565b6001600160601b03612f1082612352565b54166001600160601b0380612f2485613375565b16911610612fa957612f56612f3883613375565b610b58612f4484612352565b916110e183546001600160601b031690565b5f80808085855af1612f66612ed0565b5015611183576040519182526001600160a01b0316907f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b659080602081015b0390a2565b63112fed8b60e31b5f9081526001600160a01b0391909116600452602490fd5b335f9081525f805160206155ba833981519152602052604090205460ff1615612fee57565b63e2517d3f60e01b5f52336004525f60245260445ffd5b5f8181525f805160206155fa8339815191526020908152604080832033845290915290205460ff16156130355750565b63e2517d3f60e01b5f523360045260245260445ffd5b6001600160a01b0381165f9081525f805160206155ba833981519152602052604090205460ff166130cf576001600160a01b03165f8181525f805160206155ba83398151915260205260408120805460ff191660011790553391907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d8180a4600190565b505f90565b5f8181525f805160206155fa833981519152602090815260408083206001600160a01b038616845290915290205460ff16613176575f8181525f805160206155fa833981519152602090815260408083206001600160a01b03861684529091529020805460ff1916600117905533916001600160a01b0316907f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d5f80a4600190565b50505f90565b5f8181525f805160206155fa833981519152602090815260408083206001600160a01b038616845290915290205460ff1615613176575f8181525f805160206155fa833981519152602090815260408083206001600160a01b03861684529091529020805460ff1916905533916001600160a01b0316907ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b5f80a4600190565b906001600160401b03809116911601906001600160401b038211611d7457565b610d0f9062ffffff60406001600160401b03602084015116920151169061321c565b906001600160c11b0319821661328657602082901c6001600160a01b03169163ffffffff1690565b6341abc80160e01b5f5260045ffd5b6302000000821015611d195701905f90565b9063ffffffff166020811015613314576132f36132c8613304935460c01c90565b6132ec60036132d961090886612497565b6001600160401b038080931691161b1690565b1691612497565b6001600160401b03809216901c1690565b9060026001831615159216151590565b61335761335161334761332b602061335d956124de565b94600161334061333a88612497565b60081c90565b9101613295565b90549060031b1c90565b92612497565b60ff1690565b906003821b16901c9060026001831615159216151590565b6001600160601b03811161338f576001600160601b031690565b6306dfcc6560e41b5f52606060045260245260445ffd5b6040516323b872dd60e01b81526001600160a01b039182166004820152306024820152604481018490527f0000000000000000000000000000000000000000000000000000000000000000909116906020905f9060649082855af19081601f3d1160015f51141615166134c3575b501561348757612fa47ff645c19720906ca336d36d26058a9489c6c757fe35843b75a74e3b8aa972ecf59161346d61344b85613375565b610a0961345784612352565b91610a0483546001600160601b039060601c1690565b6040519384526001600160a01b0316929081906020820190565b60405162461bcd60e51b81526020600482015260146024820152731514905394d1915497d19493d357d1905253115160621b6044820152606490fd5b3b153d171590505f613414565b90816020910312610348575190565b9190811015611d195760051b81013590603e1981360301821215610348570190565b90821015611d19576104099160051b810190612cae565b905f5b81811061352757505050565b61353b6135358284866134df565b80611d1e565b61354961230e8486886134df565b9082820361359f575f5b83811061356757505050505060010161351b565b83811015611d19578060051b8501359061015e19863603018212156103485761359960019287016118a1838787613501565b01613553565b506377e4aa5360e11b5f5260045260245260445ffd5b90600182811c921680156135e3575b60208310146135cf57565b634e487b7160e01b5f52602260045260245ffd5b91607f16916135c4565b604051905f825f8051602061555a833981519152549161360c836135b5565b808352926001811690811561369b5750600114613630575b61035792500383610c64565b505f8051602061555a8339815191525f90815290917f42ad5d3e1f2e6e70edcf6d991b8a3023d3fca8047a131592f9edb9fd9b89d57d5b81831061367f57505090602061035792820101613624565b6020919350806001915483858901015201910190918492613667565b6020925061035794915060ff191682840152151560051b820101613624565b604051905f825f8051602061557a83398151915254916136d9836135b5565b808352926001811690811561369b57506001146136fc5761035792500383610c64565b505f8051602061557a8339815191525f90815290917f5f9ce34815f8e11431c7bb75a8e6886a91478f7ffc1dbb0a98dc240fddd76b755b81831061374b57505090602061035792820101613624565b6020919350806001915483858901015201910190918492613733565b919091608081840312610348576040519061378182610c2e565b819361378d8183612b52565b83526040820135916001600160401b038311610348576137b36060926040948301612b06565b6020850152013591612b8883610336565b919060408382031261034857604051906137dd82610c49565b81938035612b2a81611f1f565b9190916101608184031261034857613800610c94565b928135845260208201356001600160401b0381116103485781613824918401613767565b602085015260408201356001600160401b0381116103485781613848918401610cf4565b604085015260608201356001600160401b03811161034857826138728360809361387d96016137c4565b6060870152016127d2565b6080830152565b908160209103126103485751610d0f81610336565b9193926138ae6138a936856137ea565b6149c9565b946138e86138db876138be61477f565b6042916040519161190160f01b8352600283015260228201522090565b9435600160c01b16151590565b1561398b57604051630b135d3f60e11b81529260209284928391829161391391908960048501612e66565b03916001600160a01b0316620186a0fa908115610af5575f9161395c575b506001600160e01b0319166374eca2c160e11b0161394d579190565b638baa579f60e01b5f5260045ffd5b61397e915060203d602011613984575b6139768183610c64565b810190613884565b5f613931565b503d61396c565b61399a906139a0923691610cbe565b83614134565b6001600160a01b0391821691160361394d579190565b6139c49060803691016127d2565b90815160208301511061328657606082015163ffffffff16608083019063ffffffff613a006139f7845163ffffffff1690565b63ffffffff1690565b911611613286575163ffffffff1663ffffffff613a276139f760a086015163ffffffff1690565b91161161328657613a40613a3a83614a9a565b926152cd565b9162ffffff6001600160401b03613a578386613b49565b1611613286579190565b60408101916001600160401b03613a8261090885516001600160401b031690565b911690811115613b4257613a9861090883614a9a565b8111613b3b5782516001600160401b031690613acc6109086060850193613ac66139f7865163ffffffff1690565b9061321c565b811115613ade57505060209150015190565b92613b30613b3592613b28610d0f96613b22610908613b146139f7613b0960208c01518c51906124de565b965163ffffffff1690565b96516001600160401b031690565b906124de565b9451946124ad565b6124c0565b90611d67565b5050505f90565b5090505190565b906001600160401b03809116911603906001600160401b038211611d7457565b815160208301516040840151606085015160f81b6001600160f81b03191667ffffffffffffffff60a01b60a09390931b929092166001600160a01b039093169290921762ffffff60e01b60e09390931b92909216919091171781559060029060c090613c0260018501613bef613be960808501516001600160601b031690565b8261253e565b60a08301516001600160601b0316610a09565b0151910155565b9290610d0f9492613c2f9160018060a01b03168552606060208601526060850190612a32565b926040818503910152611e85565b9594919392909697613c52836108c086612352565b90613e7457613e60576001600160401b0389164211613e3f57613c7e6109b36128813660808b016127d2565b90613c8885612352565b94613c9a86546001600160601b031690565b906001600160601b0384166001600160601b03831610613e245750906001600160601b039291613cc989612352565b90613cdf82546001600160601b039060601c1690565b6101408c01359586911610613e08578c91908490036001600160601b0316613d07908961253e565b613d1085613375565b815460601c6001600160601b0316036001600160601b0316613d319161250b565b613d3a91613b49565b6001600160401b0316613d4c90614abd565b91613d5690613375565b91613d5f610c85565b6001600160a01b03891681529a6001600160401b031660208c015262ffffff1660408b01525f60608b01526001600160601b031660808a01526001600160601b031660a089015260c0880152843596613dbf885f525f60205260405f2090565b90613dc991613b69565b613dd2916152f0565b604051938493613de29385613c09565b037fe5e43c93dc0ec595ed3b122bdc6d39a480e9d17fb6812e0f90cfc4ba33b0969e91a2565b63112fed8b60e31b5f526001600160a01b038a1660045260245ffd5b63112fed8b60e31b5f526001600160a01b031660045260245ffd5b63cfe6a8fd60e01b5f5286356004526001600160401b03891660245260445ffd5b631cfdeebb60e01b5f52863560045260245ffd5b63a905765160e01b5f52873560045260245ffd5b60405190613e97606083610c64565b60268252654c696d69742960d01b6040837f43616c6c6261636b286164647265737320616464722c75696e7439362067617360208201520152565b60405190613ee1606083610c64565b60218252602960f81b6040837f496e7075742875696e743820696e707574547970652c6279746573206461746160208201520152565b60405190613f2660c083610c64565b60888252676c61746572616c2960c01b60a0837f4f666665722875696e74323536206d696e50726963652c75696e74323536206d60208201527f617850726963652c75696e7436342072616d70557053746172742c75696e743360408201527f322072616d705570506572696f642c75696e743332206c6f636b54696d656f7560608201527f742c75696e7433322074696d656f75742c75696e74323536206c6f636b436f6c60808201520152565b60405190613fe4606083610c64565b602982526874657320646174612960b81b6040837f5072656469636174652875696e743820707265646963617465547970652c627960208201520152565b60405190614031608083610c64565b605a82527f6c2c496e70757420696e7075742c4f66666572206f66666572290000000000006060837f50726f6f66526571756573742875696e743235362069642c526571756972656d60208201527f656e747320726571756972656d656e74732c737472696e6720696d616765557260408201520152565b604051906140b8608083610c64565b60438252626f722960e81b6060837f526571756972656d656e74732843616c6c6261636b2063616c6c6261636b2c5060208201527f7265646963617465207072656469636174652c6279746573342073656c65637460408201520152565b805191908290602001825e015f815290565b610d0f906138be61477f565b610d0f9161414191614ae6565b90929192614b2a565b365f80375f8036817f00000000000000000000000000000000000000000000000000000000000000005af43d5f803e15614182573d5ff35b3d5ffd5b61418e6140a9565b6141bb6141cf61419c613e88565b6141c16141a7613fd5565b6040519485936141bb602086018099614116565b90614116565b03601f198101835282610c64565b51902090565b6141dd614022565b6141bb6141cf6141eb613e88565b6141c16141f6613ed2565b6141bb614201613f17565b6141bb61420c613fd5565b916141bb6142186140a9565b956040519a8b996141bb60208c019e8f90614116565b61423b6040820151614ba6565b6142486020830151614bf2565b614290614253614186565b606085810151604080516020810194855290810196909652908501939093526001600160e01b031990921660808401529091908160a081016141c1565b5190206141cf61429e6141d5565b926141c181519160808101519060c060a08201519101519160405196879560208701998a9260a094919796959260c0850198855260208501526040840152606083015260808201520152565b9190825f525f60205280600260405f200154146143265761430a90614c63565b51614322575063c274d3e360e01b5f5260045260245ffd5b9050565b509050565b6040519061433882610c0e565b5f60c0838281528260208201528260408201528260608201528260808201528260a08201520152565b906020610d0f92818152019061040d565b604090610d0f939281528160208201520190611f33565b929391905f936143988261325e565b6143a5816108c084612352565b919092836143b161432b565b906145b1575b6143c088614c63565b946143cb8651151590565b1561453b5760208601516144ce57927fd78a37a26380237bbe8f5a5221dcf308b87fbf79aa163180e0797d675020c88b96959492888a938e965b156144ad576020810151426001600160401b03909116106144875761442a9750614fbd565b965b8751614450575b61444b60405192839260018060a01b03169683614372565b0390a3565b7f210e4fd706e561df48472433bcc50b4589f2c13e784e9992f4c3e6de26eb35646040518061447f8b82614361565b0390a1614433565b9291906144a160406144a79901516001600160601b031690565b93614dc9565b9661442c565b5050906144c760406144a79701516001600160601b031690565b9189614cad565b5050505050505090506145039193506141c1925060405192839163873fd26b60e01b6020840152602483019190602083019252565b7f210e4fd706e561df48472433bcc50b4589f2c13e784e9992f4c3e6de26eb3564604051806145328482614361565b0390a190600190565b80806145a4575b15614590576145508261323c565b6001600160401b03429116106144ce57927fd78a37a26380237bbe8f5a5221dcf308b87fbf79aa163180e0797d675020c88b96959492888a938e96614405565b63c274d3e360e01b5f52600488905260245ffd5b508860c083015114614542565b506145c66108df875f525f60205260405f2090565b6143b7565b9391610d0f9593613c2f928652606060208701526060860191611e85565b6001600160a01b039091168152604060208201819052610d0f9291019061040d565b969594929390955a603f810290808204603f1490151715611d74576001600160601b039060061c93168093106146d1576001600160a01b038716803b15610348575f956146708793604051998a988997889563a12da43f60e01b8752600487016145cb565b0393f190816146bd575b506146b9577f5c5960582bfc7a494183b4e9a66bfe8ecffc07a83a48d136e732400f7b98bf50906146a9612ed0565b90612fa4604051928392836145e9565b5050565b806122a45f6146cb93610c64565b5f61467a565b6307099c5360e21b5f5260045ffd5b90813b1561475e575f805160206155da83398151915280546001600160a01b0319166001600160a01b0384169081179091557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a2805115614746576126ee91615104565b50503461474f57565b63b398979f60e01b5f5260045ffd5b50634c9c8ce360e01b5f9081526001600160a01b0391909116600452602490fd5b614787615121565b61478f615178565b6040519060208201927f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f8452604083015260608201524660808201523060a082015260a081526141cf60c082610c64565b60ff5f8051602061561a8339815191525460401c16156147fc57565b631afcd79f60e31b5f5260045ffd5b601f8111614817575050565b5f8051602061555a8339815191525f5260205f20906020601f840160051c8301931061485d575b601f0160051c01905b818110614852575050565b5f8155600101614847565b909150819061483e565b601f821161487457505050565b5f5260205f20906020601f840160051c830193106148ac575b601f0160051c01905b8181106148a1575050565b5f8155600101614896565b909150819061488d565b9081516001600160401b038111610c29576148f5816148e25f8051602061557a833981519152546135b5565b5f8051602061557a833981519152614867565b602092601f821160011461493557614924929382915f926126f15750508160011b915f199060031b1c19161790565b5f8051602061557a83398151915255565b5f8051602061557a8339815191525f52601f198216937f5f9ce34815f8e11431c7bb75a8e6886a91478f7ffc1dbb0a98dc240fddd76b75915f5b8681106149b15750836001959610614999575b505050811b015f8051602061557a83398151915255565b01515f1960f88460031b161c191690555f8080614982565b9192602060018192868501518155019401920161496f565b6149d16141d5565b906141cf81516141c160208401516149e7614186565b90614a3a6149f58251614ba6565b6141c1614a056020850151614bf2565b6040948501518551602081019788529586019390935260608501526001600160e01b03199091166080840152829060a0820190565b5190209360408101516020815191012090614a656080614a5d60608401516151aa565b9201516151fe565b9160405196879560208701998a9260a094919796959260c0850198855260208501526040840152606083015260808201520152565b610d0f9063ffffffff60806001600160401b03604084015116920151169061321c565b62ffffff8111614acf5762ffffff1690565b6306dfcc6560e41b5f52601860045260245260445ffd5b8151919060418303614b1657614b0f9250602082015190606060408401519301515f1a90615419565b9192909190565b50505f9160029190565b60041115611e4f57565b614b3381614b20565b80614b3c575050565b614b4581614b20565b60018103614b5c5763f645eedf60e01b5f5260045ffd5b614b6581614b20565b60028103614b80575063fce698f760e01b5f5260045260245ffd5b80614b8c600392614b20565b14614b945750565b6335e2f38360e21b5f5260045260245ffd5b614bae613e88565b60208151910120906001600160601b03602060018060a01b0383511692015116604051916020830193845260408301526060820152606081526141cf608082610c64565b614bfa613fd5565b60208151910120908051906003821015611e4f576020015160208151910120614c3160405192602084019485526040840190611e42565b6060820152606081526141cf608082610c64565b60405190614c5282610c2e565b5f6040838281528260208201520152565b614c6b614c45565b505c614c75614c45565b506001600160601b0360405191614c8b83610c2e565b6001607f1b8116151583526001607e1b81161515602084015216604082015290565b9695939091929496606097614d7857614ccf614cc884612352565b948561539f565b6040519182526001600160a01b038516915f8051602061565a83398151915290602090a381546001600160601b0316906001600160601b0385166001600160601b03831610614d4157508392614d3c610b5893610b5861035797610b4695906001600160601b0391031690565b612352565b60405163112fed8b60e31b60208201526001600160a01b039091166024820152949550610d0f9350849250506044820190506141c1565b604051631cfdeebb60e01b60208201526024810191909152959650610d0f9450859350506044830191506141c19050565b906001600160601b03809116911603906001600160601b038211611d7457565b93949095979692606098614ddc86615491565b614f8a5792608092614df992614e089515614f4b575b5050612352565b9301516001600160601b031690565b935f928495856001600160601b0382166001600160601b038216115f14614f1b5781614e3391614da9565b90614e4583546001600160601b031690565b906001600160601b0383166001600160601b03831610614ee1575b5093614e88614e8d946117938395610b58614d3c96614ea29a906001600160601b0391031690565b6154b4565b610b5885610a0483546001600160601b031690565b614eaa575050565b604051636008fdcb60e01b60208201526001600160601b03918216602482015291166044820152909150610d0f81606481016141c1565b975094505091614d3c81614e88614e8d94611793614ea297610b58614f078b809e6124eb565b9c60019b9650965050959750509450614e60565b93614e88614e8d946117938395610b58614f3b614ea29a614d3c98614da9565b82546001600160601b03166124eb565b614f5d90614f5884612352565b61539f565b6040519081526001600160a01b0386169089905f8051602061565a83398151915290602090a35f80614df2565b5050604051631cfdeebb60e01b6020820152602481019690965250949550929350610d0f925083915050604481016141c1565b9391909296959496606097614fd186615491565b6150d3571561509a575b505082516001600160a01b038581169116148015919061508b575b5061505f57613457610b4060a0610357959461503c61501f610a09965f525f60205260405f2090565b80546001600160f81b0316600160f81b1781555f60019190910155565b610b3261505360808301516001600160601b031690565b610b58610b4689612352565b60405163a905765160e01b60208201526024810191909152929350610d0f9150829050604481016141c1565b905060c083015114155f614ff6565b614f586150a692612352565b6040518181526001600160a01b0385169083905f8051602061565a83398151915290602090a35f80614fdb565b5050604051631cfdeebb60e01b60208201526024810193909352509394509250610d0f9150829050604481016141c1565b5f80610d0f93602081519101845af461511b612ed0565b916154fb565b6151296135ed565b8051908115615139576020012090565b50505f8051602061559a8339815191525480156151535790565b507fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a47090565b6151806136ba565b8051908115615190576020012090565b50505f8051602061563a8339815191525480156151535790565b6151b2613ed2565b602081519101209060208151916151c883611f29565b01516020815191012060405191602083019384526151e581611f29565b60408301526060820152606081526141cf608082610c64565b615206613f17565b60405161521b816141c1602082018095614116565b519020906141cf81516141c160208401519361524160408201516001600160401b031690565b90615253606082015163ffffffff1690565b608082015163ffffffff169060c061527260a085015163ffffffff1690565b93015193604051988997602089019b8c9463ffffffff94906001600160401b0386949260e099949c9b9a9686946101008b019e8b5260208b015260408a01521660608801521660808601521660a08401521660c08201520152565b610d0f9063ffffffff60a06001600160401b03604084015116920151169061321c565b9063ffffffff166020811015615349579061532561531361090861035794612497565b60016001600160401b039182161b1690565b815460c01c82546001600160c01b0316911760c01b6001600160c01b031916179055565b60208103908111611d745761537c61035792600161537260ff61536b86612497565b1694612497565b60081c9101613295565b81545f1960039290921b91821b198116600190941b90821c17901b919091179055565b9063ffffffff1660208110156153d457906153256153c261090861035794612497565b60026001600160401b039182161b1690565b60208103908111611d74576153f661035792600161537260ff61536b86612497565b81545f1960039290921b91821b198116600290941b90821c17901b919091179055565b91906fa2a8918ca85bafe22016d0b997e4df60600160ff1b038411615486579160209360809260ff5f9560405194855216868401526040830152606082015282805260015afa15610af5575f516001600160a01b0381161561547c57905f905f90565b505f906001905f90565b5050505f9160039190565b606081015160011615159081156154a6575090565b606001516002161515905090565b80546001600160a01b0319166001600160a01b039092169190911781556103579080546001600160f81b03811660f891821c60021790911b6001600160f81b031916179055565b9061551f575080511561551057602081519101fd5b63d6bda27560e01b5f5260045ffd5b81511580615550575b615530575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b1561552856fea16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d102a16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d103a16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100b7db2dd08fcb62d0c9e08c51941cae53c267786a0b75803fb7960902fc8ef97d360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc02dd7bc7dec4dceedda775e58dd541e08a116c6c53815c0bd028192f7b626800f0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00a16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d101120ea8d7610aa46e4a31b254c5d07489ebe8f1a93dc7bbbe60eaf3db2c62c0cca164736f6c634300081a000a")] contract BoundlessMarket { - constructor(address router, address collateralTokenContract) {} + constructor(address router, address collateralTokenContract, address legacyImpl) {} function initialize(address initialOwner) {} } } diff --git a/crates/test-utils/src/market.rs b/crates/test-utils/src/market.rs index b6271bcec..e9c6f74c8 100644 --- a/crates/test-utils/src/market.rs +++ b/crates/test-utils/src/market.rs @@ -230,6 +230,14 @@ pub async fn deploy_router( Ok(*proxy_instance.address()) } +/// Placeholder `legacyImpl` for the new `BoundlessMarket` constructor, which rejects the zero +/// address. The legacy fallback exists only so pre-router clients can keep hitting the deployed +/// contract during migration; nothing in the current broker/SDK should ever route through it, so +/// tests deliberately point it at a non-functional address instead of deploying a real +/// `BoundlessMarketLegacy`. Any call that did reach the fallback would fail to return usable data, +/// surfacing the misuse rather than silently succeeding. +const LEGACY_IMPL_STUB: Address = Address::new([0xde; 20]); + #[allow(clippy::too_many_arguments)] pub async fn deploy_boundless_market( owner_address: Address, @@ -249,9 +257,10 @@ pub async fn deploy_boundless_market( ) .await?; - let market_instance = BoundlessMarket::deploy(&deployer_provider, router, hit_points) - .await - .context("failed to deploy BoundlessMarket implementation")?; + let market_instance = + BoundlessMarket::deploy(&deployer_provider, router, hit_points, LEGACY_IMPL_STUB) + .await + .context("failed to deploy BoundlessMarket implementation")?; let proxy_instance = ERC1967Proxy::deploy( &deployer_provider, diff --git a/justfile b/justfile index e8ef34bb6..de645bfa1 100644 --- a/justfile +++ b/justfile @@ -149,10 +149,22 @@ test-db action="setup": fi # Run all formatting and linting checks -check: check-links check-license check-format check-clippy +check: check-links check-license check-format check-clippy check-legacy-bytecode check-storage-layout check-main: check-format-main check-clippy-main check-license check-links +# Verify contracts/src/legacy/ still compiles to the deployed OLD market bytecode +check-legacy-bytecode: + @echo "Verifying legacy market bytecode parity..." + forge build --silent + uv run contracts/scripts/verify-legacy-bytecode.py + +# Verify storage layout interop between src/ and src/legacy/ markets +check-storage-layout: + @echo "Verifying storage layout interop between markets..." + forge build --silent + uv run contracts/scripts/verify-storage-layout.py + # Check links in markdown files check-links: @echo "Checking links in markdown files..." diff --git a/license-check.py b/license-check.py index b5a9f7dab..d4899bc9b 100755 --- a/license-check.py +++ b/license-check.py @@ -60,6 +60,7 @@ str(Path.cwd()) + "/blake3_groth16", str(Path.cwd()) + "/contracts/src/HitPoints.sol", str(Path.cwd()) + "/contracts/src/IBoundlessMarket.sol", + str(Path.cwd()) + "/contracts/src/legacy/IBoundlessMarketLegacy.sol", str(Path.cwd()) + "/contracts/src/IHitPoints.sol", str(Path.cwd()) + "/contracts/src/povw/IPovwAccounting.sol", str(Path.cwd()) + "/contracts/src/povw/IPovwMint.sol",