Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
789075e
chore(contracts): import main's BoundlessMarket sources into legacy/
jonastheis May 28, 2026
2e2b127
test(contracts): port main's BoundlessMarket suite into test/legacy/
jonastheis May 28, 2026
6a85429
chore(contracts): verify legacy/ bytecode parity against deployed OLD…
jonastheis May 28, 2026
10e4380
chore(contracts): enforce storage layout interop between src/ and src…
jonastheis May 28, 2026
9015ff0
feat(contracts): forward legacy ABI to a configurable impl via fallback
jonastheis May 28, 2026
d90b32c
test(contracts): exercise legacy ABI against the new market via fallback
jonastheis May 28, 2026
cd1d6a2
test(contracts): pin cross-ABI invariants in a focused suite
jonastheis May 28, 2026
082190b
chore(contracts): wire legacy impl into Deploy.s.sol and unbreak depl…
jonastheis May 29, 2026
65f6640
docs(contracts): document the legacy/ freeze policy and provenance
jonastheis May 29, 2026
93987d8
refactor(contracts): forward legacy ABI via OpenZeppelin Proxy
jonastheis Jun 5, 2026
3cbef17
refactor(contracts): de-duplicate legacy ABI test suites via inheritance
jonastheis Jun 5, 2026
3db3efe
Merge branch 'jonas/router-decoupling' into jonas/market-legacy-fallback
jonastheis Jun 5, 2026
d1f79be
fix(test-utils): pass legacyImpl to the 3-arg BoundlessMarket constru…
jonastheis Jun 8, 2026
9cb7f66
style(contracts): satisfy dprint and forge fmt checks
jonastheis Jun 8, 2026
0d06344
fix(contracts): resolve legacyImpl in manage deploy/upgrade scripts
jonastheis Jun 8, 2026
ee3c0f5
test(contracts): migrate deployment-test to the new batched ABI
jonastheis Jun 8, 2026
b343ed6
test(contracts): stub the legacy impl in the BoundlessMarket harness
jonastheis Jun 8, 2026
c248f56
test(contracts): refresh market snapshots after batched-ABI migration
jonastheis Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
16 changes: 11 additions & 5 deletions contracts/deployment-test/Deploymnet.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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"
);
}
Expand All @@ -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"
);
}
Expand Down Expand Up @@ -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);
Expand Down
19 changes: 18 additions & 1 deletion contracts/scripts/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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}(
Expand Down
44 changes: 34 additions & 10 deletions contracts/scripts/Manage.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -71,21 +72,41 @@ 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)))
);

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"
);
Expand Down Expand Up @@ -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.
Expand All @@ -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!");
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
Loading