From cd387007e79f0791c775878ebad2950ab8ca1667 Mon Sep 17 00:00:00 2001 From: FlowMemory HQ Agent Date: Wed, 13 May 2026 22:38:22 -0500 Subject: [PATCH] Add real-value pilot contracts proof --- contracts/ACCESS_CONTROL_REVIEW.md | 77 ++++++ contracts/DEPLOYMENT_BOUNDARY.md | 20 +- contracts/FlowChainSettlementSpine.sol | 3 + contracts/bridge/BaseBridgeLockbox.sol | 8 +- docs/FLOWCHAIN_REAL_VALUE_PILOT.md | 33 +-- .../real-value-pilot-contracts/CHECKLIST.md | 32 +++ .../real-value-pilot-contracts/EXPERIMENTS.md | 35 +++ .../real-value-pilot-contracts/NOTES.md | 46 ++++ .../real-value-pilot-contracts/PLAN.md | 74 ++++++ docs/bridge/FLOWCHAIN_BASE_BRIDGE_POC.md | 138 ++++++++++- ...owchain-real-value-pilot-contracts-e2e.ps1 | 224 ++++++++++++++++++ package.json | 1 + script/DeployBridgeSpine.s.sol | 68 +++++- tests/FlowChainSettlementSpine.t.sol | 99 +++++++- tests/bridge/BaseBridgeLockbox.t.sol | 35 +++ 15 files changed, 844 insertions(+), 49 deletions(-) create mode 100644 docs/agent-runs/real-value-pilot-contracts/CHECKLIST.md create mode 100644 docs/agent-runs/real-value-pilot-contracts/EXPERIMENTS.md create mode 100644 docs/agent-runs/real-value-pilot-contracts/NOTES.md create mode 100644 docs/agent-runs/real-value-pilot-contracts/PLAN.md create mode 100644 infra/scripts/flowchain-real-value-pilot-contracts-e2e.ps1 diff --git a/contracts/ACCESS_CONTROL_REVIEW.md b/contracts/ACCESS_CONTROL_REVIEW.md index c4de6767..66217bb9 100644 --- a/contracts/ACCESS_CONTROL_REVIEW.md +++ b/contracts/ACCESS_CONTROL_REVIEW.md @@ -10,6 +10,83 @@ They also do not enforce cross-contract dependency existence. For example, a wor No current contract exposes bridge finality or a challenge lifecycle. `REORGED` is an allowed verifier-report status for local/test reconciliation, not a Solidity finality proof, production bridge state, or challenge-resolution mechanism. +## BaseBridgeLockbox + +Owner model: one constructor `initialOwner` controls the lockbox configuration. + +Owner-gated functions: + +- `transferOwnership` +- `setReleaseAuthority` +- `setPaused` +- `configureToken` + +Release-authority-gated functions: + +- `releaseNative` +- `releaseERC20` + +Current protections: + +- zero owner and zero release authority rejected +- only allowlisted tokens can be deposited +- allowed tokens require a nonzero per-deposit cap +- optional per-asset total cap prevents total locked accounting from exceeding + the configured pilot cap +- total cap cannot be lowered below currently locked amount +- pause blocks new deposits +- releases require explicit release authority, a recorded deposit, matching + token, available amount, and nonzero evidence hash +- release replay is blocked for identical deposit, recipient, token, amount, and + evidence hash +- direct native transfers outside `lockNative` are rejected + +Launch risk to watch: + +- pause intentionally does not block releases, so pilot operators can unwind + deposits while deposits are stopped. +- release authority is a trusted pilot role, not a decentralized validator set + or finality proof. +- nonstandard ERC-20 behavior such as transfer fees, rebasing, or callbacks is + outside the pilot safety claim. +- native releases use Solidity `transfer`; gas-heavy smart-contract recipients + can fail and should not be used for pilot recovery without separate review. +- a compromised owner or release authority can misuse the POC; emergency + response is limited to pause, cap changes, allowlist disablement, authority + rotation, and explicit release/recovery calls. + +## FlowChainSettlementSpine + +Owner model: one constructor `initialOwner` controls submitter authorization. + +Owner-gated functions: + +- `transferOwnership` +- `setSubmitterAuthorization` + +Submitter-gated functions: + +- `commitObject` requires an authorized submitter. + +Current protections: + +- zero owner rejected +- zero submitter rejected for authorization changes +- owner is authorized as the first submitter +- unauthorized submitters cannot commit objects +- zero object type, object id, rootfield id, and commitment rejected +- duplicate object ids rejected +- committed object records can be read by object id + +Launch risk to watch: + +- submitter authorization is a coordination control, not proof of object + correctness. +- unknown object types are allowed so local experiments can proceed, but + downstream agents should treat unknown types as unsupported until documented. +- events omit `txHash`, `logIndex`, receipt status, and finality status; + indexers derive locator fields after receipts and logs exist. + ## RootfieldRegistry Owner model: each `rootfieldId` has one owner. diff --git a/contracts/DEPLOYMENT_BOUNDARY.md b/contracts/DEPLOYMENT_BOUNDARY.md index 1254b476..a6e73f42 100644 --- a/contracts/DEPLOYMENT_BOUNDARY.md +++ b/contracts/DEPLOYMENT_BOUNDARY.md @@ -1,6 +1,6 @@ # Contracts Deployment Boundary -Status: V0 local and Base Sepolia readiness boundary. +Status: V0 local, Base Sepolia, and capped Base public-network pilot boundary. The current contracts are a compact event and commitment spine. They store intentional roots, receipt/report commitments, registry metadata hashes, counters, and status fields only. Heavy artifacts, AI memory, media, model data, verifier evidence, and receipt reconstruction data remain off-chain. @@ -15,6 +15,10 @@ For the private/local FlowChain testnet package, these Solidity contracts are op including CREATE2 salt mining for the exact hook flag target. - Base Sepolia reads from explicit RPC URLs. - Guarded Base mainnet canary reads and source-verification dry runs for the documented V0 canary addresses only. +- Capped Base chain id `8453` bridge-pilot dry runs and explicit broadcasts for + `BaseBridgeLockbox` and `FlowChainSettlementSpine` only, with local env + acknowledgement, explicit owner/release authority, allowlisted assets, and + nonzero configured total caps. - Public docs that describe emitted events, roots, receipts, and off-chain verification paths. ## Not Allowed Yet @@ -28,7 +32,7 @@ For the private/local FlowChain testnet package, these Solidity contracts are op - Broad Base mainnet scans outside the documented canary reader guardrails. - Token launch, rewards, slashing, or fee-market mechanics. - Dynamic Uniswap v4 fee hooks. -- Custody of user tokens. +- Uncapped or unreviewed custody of user tokens. - Claims that contracts can know `txHash` or `logIndex` during execution. - Claims that on-chain storage is free or that arbitrary AI data is stored on-chain. @@ -140,6 +144,13 @@ outside Git. The detailed public testnet rehearsal runbook is `docs/DEPLOYMENTS/BASE_SEPOLIA_REHEARSAL.md`. +`script/DeployBridgeSpine.s.sol` is a separate dry-run-by-default bridge-spine +script for local Anvil `31337`, Base Sepolia `84532`, and the capped Base +`8453` pilot. The `8453` path requires `FLOWCHAIN_BASE8453_PILOT_ACK=true` and +nonzero total caps for every configured asset. The script deploys the existing +lockbox and settlement spine only; it does not create a new bridge architecture +or broad public bridge approval. + `verify:base-canary:sources` reads `fixtures/deployments/base-canary-v0.json` and prints a dry-run verification plan by default. It also writes the same non-secret plan to @@ -164,6 +175,11 @@ submission uses `npm run verify:base-canary:sources:submit` and requires - `ArtifactRegistry`: artifact commitment metadata. - `CursorRegistry`: off-chain cursor commitment metadata. - `WorkDebtScheduler`: work-state commitments without token debt. +- `BaseBridgeLockbox`: capped bridge-pilot lockbox with owner configuration, + explicit release authority, pause, allowlisted assets, per-deposit caps, + per-asset total caps, deposit replay guards, and release replay guards. +- `FlowChainSettlementSpine`: object commitment event spine for bridge, + control-plane, memory, and finality object references. ## Post-Deploy Checks diff --git a/contracts/FlowChainSettlementSpine.sol b/contracts/FlowChainSettlementSpine.sol index b423e6e2..2f285290 100644 --- a/contracts/FlowChainSettlementSpine.sol +++ b/contracts/FlowChainSettlementSpine.sol @@ -18,6 +18,9 @@ contract FlowChainSettlementSpine { } bytes32 public constant BRIDGE_DEPOSIT_OBJECT = keccak256("flowchain.object.bridge-deposit.v0"); + bytes32 public constant BRIDGE_CREDIT_OBJECT = keccak256("flowchain.object.bridge-credit.v0"); + bytes32 public constant BRIDGE_WITHDRAWAL_INTENT_OBJECT = + keccak256("flowchain.object.bridge-withdrawal-intent.v0"); bytes32 public constant MEMORY_OBJECT = keccak256("flowchain.object.memory.v0"); bytes32 public constant FINALITY_OBJECT = keccak256("flowchain.object.finality.v0"); diff --git a/contracts/bridge/BaseBridgeLockbox.sol b/contracts/bridge/BaseBridgeLockbox.sol index a6b0c4b4..a9c6e561 100644 --- a/contracts/bridge/BaseBridgeLockbox.sol +++ b/contracts/bridge/BaseBridgeLockbox.sol @@ -204,11 +204,11 @@ contract BaseBridgeLockbox { nonReentrant returns (bytes32 releaseId) { - releaseId = _recordRelease(depositId, recipient, NATIVE_TOKEN, amount, evidenceHash); - (bool ok,) = recipient.call{value: amount}(""); - if (!ok) { - revert TransferFailed(); + if (recipient == address(0)) { + revert ZeroRecipient(); } + releaseId = _recordRelease(depositId, recipient, NATIVE_TOKEN, amount, evidenceHash); + recipient.transfer(amount); } function releaseERC20(bytes32 depositId, address recipient, address token, uint256 amount, bytes32 evidenceHash) diff --git a/docs/FLOWCHAIN_REAL_VALUE_PILOT.md b/docs/FLOWCHAIN_REAL_VALUE_PILOT.md index 38d58c6e..5db314c2 100644 --- a/docs/FLOWCHAIN_REAL_VALUE_PILOT.md +++ b/docs/FLOWCHAIN_REAL_VALUE_PILOT.md @@ -19,8 +19,8 @@ approval. ## Current Baseline -Current `main` after PR #144 merged at -`6272bf1f41761ddd5cb80a0b780fd000d74b5026`: +Current `main` after PR #145 merged at +`91b4d5d033857f1d10526912d852d13ff2e86a23`: - `npm run flowchain:product-e2e` exists as the local product testnet gate. - `npm run flowchain:full-smoke` exists as the private/local L1 baseline gate. @@ -35,6 +35,8 @@ Current `main` after PR #144 merged at #143 merged. - `npm run flowchain:real-value-pilot:ops` exists on `main` after PR #144 merged. +- `npm run flowchain:real-value-pilot:bridge` exists on `main` after PR #145 + merged. GitHub source-of-truth state checked for this pass: @@ -48,8 +50,9 @@ GitHub source-of-truth state checked for this pass: - Issue #136 is closed; PR #143 merged the wallet/operator pilot proof command. - Issue #135 is closed; PR #144 merged the ops/installer pilot proof command. -- Issues #133, #138, and #134 remain the open subsystem proof blockers for - strict pilot-gate pass. +- Issue #138 is closed; PR #145 merged the bridge relayer pilot proof command. +- Issues #133 and #134 remain the open subsystem proof blockers for strict + pilot-gate pass. ## Final Gate @@ -139,11 +142,11 @@ the proof is branch-local or verified from `main`. | --- | --- | --- | --- | | Existing product testnet gate remains green. | HQ/Ops | `npm run flowchain:product-e2e` | Existing command; run before PR when practical. | | L1 baseline gate remains green. | HQ/Ops | `npm run flowchain:l1-e2e` | Exists on `main` as current alias to `flowchain:full-smoke`; latest local main-equivalent run passed. | -| Base chain ID `8453` is verified before any live observer or deployment action. | Contracts + Bridge + Ops | `npm run flowchain:real-value-pilot:contracts`; `npm run flowchain:real-value-pilot:bridge`; `npm run flowchain:real-value-pilot:ops` | Contracts command is still missing; bridge branch command added here pending PR merge; ops is merged. | -| Lockbox address is loaded from ignored local config or env, not hardcoded as a blanket endorsement. | Contracts + Ops | `npm run flowchain:real-value-pilot:contracts`; `npm run flowchain:real-value-pilot:ops` | Contracts command is still missing; ops is merged. | -| Per-deposit cap, total pilot cap, supported-asset allowlist, pause, release, recovery, and replay protection are covered by tests and dry-run deployment evidence. | Contracts | `npm run flowchain:real-value-pilot:contracts` | Missing dedicated pilot command. | -| Deposit observation writes deterministic observation, credit, and evidence files. | Bridge relayer | `npm run flowchain:real-value-pilot:bridge` | Branch command added here; local proof passes, pending PR merge. | -| Duplicate Base event replay is rejected or idempotent with explicit evidence. | Bridge relayer + Chain runtime | `npm run flowchain:real-value-pilot:bridge`; `npm run flowchain:real-value-pilot:runtime` | Bridge branch command added here; runtime command still missing. | +| Base chain ID `8453` is verified before any live observer or deployment action. | Contracts + Bridge + Ops | `npm run flowchain:real-value-pilot:contracts`; `npm run flowchain:real-value-pilot:bridge`; `npm run flowchain:real-value-pilot:ops` | Contracts branch command added here; bridge and ops are merged. | +| Lockbox address is loaded from ignored local config or env, not hardcoded as a blanket endorsement. | Contracts + Ops | `npm run flowchain:real-value-pilot:contracts`; `npm run flowchain:real-value-pilot:ops` | Contracts branch command added here; ops is merged. | +| Per-deposit cap, total pilot cap, supported-asset allowlist, pause, release, recovery, and replay protection are covered by tests and dry-run deployment evidence. | Contracts | `npm run flowchain:real-value-pilot:contracts` | Branch command added here; local proof passes, pending PR merge. | +| Deposit observation writes deterministic observation, credit, and evidence files. | Bridge relayer | `npm run flowchain:real-value-pilot:bridge` | Merged on `main` by PR #145; latest local main-equivalent proof passed. | +| Duplicate Base event replay is rejected or idempotent with explicit evidence. | Bridge relayer + Chain runtime | `npm run flowchain:real-value-pilot:bridge`; `npm run flowchain:real-value-pilot:runtime` | Bridge proof is merged; runtime command still missing. | | Local runtime applies each pilot bridge credit exactly once and preserves state across restart/export/import. | Chain runtime | `npm run flowchain:real-value-pilot:runtime` | Missing dedicated pilot command. | | Operator wallet can sign pilot acknowledgements, withdrawal intents, release evidence, and emergency messages without committing secrets. | Wallet/operator | `npm run flowchain:real-value-pilot:wallet` | Merged on `main` by PR #143; latest local main-equivalent proof passed. | | Wallet verification rejects wrong chain ID, wrong contract, wrong operator, mutated payload, replay nonce, expired message, and missing cap fields. | Wallet/operator | `npm run flowchain:real-value-pilot:wallet` | Merged on `main` by PR #143; latest local main-equivalent proof passed. | @@ -162,9 +165,9 @@ from `main`. | Area | In-flight branch state | Required next step | | --- | --- | --- | -| Contracts | `agent/real-value-pilot-contracts` checklist reports the contracts proof complete, including hardening, deploy dry-run, and product E2E. | Rebase onto `6272bf1`, expose `flowchain:real-value-pilot:contracts`, rerun evidence, and open a PR. | -| Bridge relayer | This branch adapts `agent/real-value-pilot-bridge` work onto `6272bf1` and exposes branch-local `flowchain:real-value-pilot:bridge`. | Open a PR for issue #138 so the proof command lands on `main`. | -| Chain runtime | `agent/real-value-pilot-chain` checklist reports runtime credit/replay/restart/export proof complete through the direct wrapper; root package command is missing. | Rebase onto `6272bf1`, expose `flowchain:real-value-pilot:runtime`, rerun evidence, and open a PR. | +| Contracts | This branch adapts `agent/real-value-pilot-contracts` work onto `91b4d5d` and exposes branch-local `flowchain:real-value-pilot:contracts`. | Open a PR for issue #133 so the proof command lands on `main`. | +| Bridge relayer | `flowchain:real-value-pilot:bridge` merged on `main` through PR #145 and closed issue #138. | No bridge relayer blocker remains for the final pilot gate. | +| Chain runtime | `agent/real-value-pilot-chain` checklist reports runtime credit/replay/restart/export proof complete through the direct wrapper; root package command is missing. | Rebase onto `91b4d5d`, expose `flowchain:real-value-pilot:runtime`, rerun evidence, and open a PR. | | Wallet/operator | `flowchain:real-value-pilot:wallet` merged on `main` through PR #143 and closed issue #136. | No wallet/operator blocker remains for the final pilot gate. | | Control plane/dashboard | `flowchain:real-value-pilot:control-dashboard` merged on `main` through PR #142 and closed issue #137. | No control-dashboard blocker remains for the final pilot gate. | | Ops/installer | `flowchain:real-value-pilot:ops` merged on `main` through PR #144 and closed issue #135. | No ops/installer blocker remains for the final pilot gate. | @@ -194,8 +197,8 @@ in committed files, or if any document presents the pilot as public readiness. ## Current Blockers -- Dedicated real-value contracts gate does not exist; tracked by issue #133. -- Dedicated real-value bridge relayer gate exists branch-locally and passes; tracked by issue #138 until merged. +- Dedicated real-value contracts gate exists branch-locally and passes; tracked by issue #133 until merged. +- Dedicated real-value bridge relayer gate is merged on `main`; issue #138 is closed by PR #145. - Dedicated real-value runtime gate does not exist; tracked by issue #134. - Dedicated real-value wallet/operator gate is merged on `main`; issue #136 is closed by PR #143. - Dedicated real-value control-plane/dashboard gate is merged on `main`; issue #137 is closed by PR #142. @@ -211,7 +214,7 @@ in committed files, or if any document presents the pilot as public readiness. | Area | Issue | Required command | | --- | --- | --- | | Contracts | #133 | `npm run flowchain:real-value-pilot:contracts` | -| Bridge relayer | #138 | `npm run flowchain:real-value-pilot:bridge` | +| Bridge relayer | #138, closed by PR #145 | `npm run flowchain:real-value-pilot:bridge` | | Chain runtime | #134 | `npm run flowchain:real-value-pilot:runtime` | | Wallet/operator | #136, closed by PR #143 | `npm run flowchain:real-value-pilot:wallet` | | Control plane/dashboard | #137, closed by PR #142 | `npm run flowchain:real-value-pilot:control-dashboard` | diff --git a/docs/agent-runs/real-value-pilot-contracts/CHECKLIST.md b/docs/agent-runs/real-value-pilot-contracts/CHECKLIST.md new file mode 100644 index 00000000..f2793d0c --- /dev/null +++ b/docs/agent-runs/real-value-pilot-contracts/CHECKLIST.md @@ -0,0 +1,32 @@ +# Real-Value Pilot Contracts Checklist + +## Acceptance + +- [x] `forge test` passes. +- [x] `npm run contracts:hardening` passes. +- [x] Lockbox supports chain ID `8453` deployment configuration. +- [x] Contract enforces per-deposit cap and total pilot cap. +- [x] Contract supports allowlisted asset(s) only. +- [x] Pause blocks deposits. +- [x] Authorized release/recovery path remains possible while paused. +- [x] Replay protection prevents duplicate release/deposit accounting. +- [x] Events contain deterministic relayer inputs without contract-side + `txHash`/`logIndex` assumptions. +- [x] Dry-run deployment script exists. +- [x] Broadcast deployment script requires explicit local env ack and never + commits keys. +- [x] Verification/source command or instructions exist. +- [x] Contract docs explain owner, release authority, cap, pause, replay, and + emergency assumptions. +- [x] `npm run flowchain:product-e2e` still passes or breakage is assigned. + +## Work Items + +- [x] Read required repo docs. +- [x] Inspect current main contracts and tests. +- [x] Inspect `E:\FlowMemory\flowmemory-contracts` active long-loop work. +- [x] Inspect `E:\FlowMemory\flowmemory-bridge-full` event expectations. +- [x] Update settlement object vocabulary and tests. +- [x] Update deployment gating for Base `8453` pilot. +- [x] Update bridge and deployment docs. +- [x] Run verification commands and record exact results. diff --git a/docs/agent-runs/real-value-pilot-contracts/EXPERIMENTS.md b/docs/agent-runs/real-value-pilot-contracts/EXPERIMENTS.md new file mode 100644 index 00000000..3b758401 --- /dev/null +++ b/docs/agent-runs/real-value-pilot-contracts/EXPERIMENTS.md @@ -0,0 +1,35 @@ +# Real-Value Pilot Contracts Experiments + +## Commands + +Commands will be recorded here with pass/fail status and concise evidence. + +| Command | Status | Notes | +| --- | --- | --- | +| `forge test --match-path tests/bridge/BaseBridgeLockbox.t.sol` | pass | 17 passed, 0 failed. | +| `forge test --match-path tests/FlowChainSettlementSpine.t.sol` | pass | 8 passed, 0 failed. | +| `npm run flowchain:real-value-pilot:contracts` | pass | Focused tests passed, `contracts:hardening` passed with 87 tests, local Anvil dry run passed, Base `8453` missing ack rejected, Base `8453` acknowledged dry run passed, and report written under `devnet/local/real-value-pilot/contracts-e2e/`. | +| Local Anvil `forge script` dry run | pass | `DeployBridgeSpine` simulated on chain `31337` from the root proof wrapper. | +| Base `8453` missing-ack dry run | pass | Rejected with `Base8453PilotAckRequired`. | +| Base `8453` acknowledged dry run | pass | Simulated on chain `8453` with `FLOWCHAIN_BASE8453_PILOT_ACK=true` and nonzero native total cap. | +| `npm run flowchain:product-e2e` | pass | Product Testnet V1 E2E passed. Generated outputs were restored afterward. | +| `npm run flowchain:l1-e2e` | pass | Private/local L1 full-smoke alias passed. Generated outputs were restored afterward. | +| `git diff --check` | pass | Exit 0; Git printed CRLF normalization warnings only. | +| `node infra/scripts/check-unsafe-claims.mjs` | pass | Unsafe-claim scan passed. | +| `npm run flowchain:real-value-pilot:e2e -- -AllowIncomplete` | pass | Incomplete coordination report now lists only runtime #134 missing. | +| `npm run flowchain:real-value-pilot:e2e` | expected fail | Strict final gate fails clearly with only runtime #134 missing. | + +## Additional Commands + +- `forge fmt --check contracts/FlowChainSettlementSpine.sol script/DeployBridgeSpine.s.sol tests/FlowChainSettlementSpine.t.sol tests/bridge/BaseBridgeLockbox.t.sol`: the source branch recorded existing line-ending/format normalization noise; no broad formatter pass is included in this integration branch. +- `contracts:hardening` keeps Slither optional by default after the HQ policy merge. Explicit Slither audit remains `npm run contracts:hardening:slither` and is outside this contracts proof PR. + +## Findings + +- The relayer already supports Base `8453` observations in its schema and code, + but Base mainnet canary mode is read-only on the bridge-full side. +- Contract-side work should preserve the existing `BridgeDeposit` ABI so the + relayer's parser remains compatible. +- `DeployBridgeSpine` now gates local Anvil `31337`, Base Sepolia `84532`, and + Base `8453`; the `8453` path requires `FLOWCHAIN_BASE8453_PILOT_ACK=true` and + nonzero total caps for configured assets. diff --git a/docs/agent-runs/real-value-pilot-contracts/NOTES.md b/docs/agent-runs/real-value-pilot-contracts/NOTES.md new file mode 100644 index 00000000..8bec018c --- /dev/null +++ b/docs/agent-runs/real-value-pilot-contracts/NOTES.md @@ -0,0 +1,46 @@ +# Real-Value Pilot Contracts Notes + +## Source Context + +- `docs/START_HERE.md`, `docs/FLOWMEMORY_HQ_CONTEXT.md`, + `docs/CURRENT_STATE.md`, `docs/ROOTFLOW_V0.md`, + `docs/FLOW_MEMORY_V0.md`, `docs/V0_LAUNCH_ACCEPTANCE.md`, and + `docs/PR_PROCESS.md` were read before editing. +- This integration branch ports the useful contract-side work from + `E:\FlowMemory\flowmemory-live-contracts` onto current `main` after PR #145. + Several sibling worktrees are dirty; their changes are context only until + merged. +- The real-value pilot goal pack exists in `E:\FlowMemory\flowchain-release`, + not in this branch. + +## Event Boundary + +`BridgeDeposit` must remain: + +```solidity +BridgeDeposit(bytes32,uint256,address,address,uint256,bytes32,uint256,bytes32) +``` + +Receipt fields such as `txHash`, `transactionIndex`, `logIndex`, block number, +and block hash are relayer/indexer-derived after logs exist. The contract emits +deterministic in-transaction data only: schema-derived `depositId`, chain id, +lockbox address by log emitter, sender, token, amount, FlowChain recipient, +nonce, and metadata hash. + +## Pilot Boundary + +The `8453` path is a capped pilot deployment configuration for the same lockbox +and settlement spine, not a broad public bridge claim. Owner and release +authority are trusted pilot roles. Emergency controls are pause, cap changes +above current locked amount, allowlist disablement, authority rotation, and +authorized release/recovery calls. + +## Verification Notes + +- `npm run flowchain:product-e2e` regenerates tracked fixture/dashboard/service + outputs during the run. Those generated outputs were restored afterward so the + branch stays inside the assigned folders. +- Foundry dry-run artifacts under `broadcast/` and cache artifacts under + `cache/` remain ignored by Git. +- The root package alias is `flowchain:real-value-pilot:contracts`; the final + HQ gate remains `flowchain:real-value-pilot:e2e`. diff --git a/docs/agent-runs/real-value-pilot-contracts/PLAN.md b/docs/agent-runs/real-value-pilot-contracts/PLAN.md new file mode 100644 index 00000000..aa204bd8 --- /dev/null +++ b/docs/agent-runs/real-value-pilot-contracts/PLAN.md @@ -0,0 +1,74 @@ +# Real-Value Pilot Contracts Plan + +Status: implemented on branch `agent/real-value-pilot-contracts-proof`; +pending PR for issue #133. + +Worktree: `E:\FlowMemory\flowmemory-live-wallet` +Branch: `agent/real-value-pilot-contracts-proof` + +## Scope + +Build the contract side of the capped Base public-network pilot bridge without +creating a parallel bridge architecture. Reuse the existing `BaseBridgeLockbox`, +`FlowChainSettlementSpine`, tests, and deployment script. + +Allowed edit folders: + +- `contracts/` +- `tests/` +- `script/` +- `infra/scripts/flowchain-real-value-pilot-contracts-e2e.ps1` +- `package.json` +- `docs/bridge/` +- `docs/agent-runs/real-value-pilot-contracts/` + +Forbidden edit folders: + +- `crates/` +- `services/` +- `apps/dashboard/` +- `crypto/` +- `hardware/` + +## Inspection Notes + +- This integration branch starts from current `main` after PR #145. +- Existing lockbox already has token allowlisting, per-deposit caps, per-asset + total caps, pause, release authority, deposit records, deposit replay IDs, + release replay IDs, and relayer-facing events. +- `E:\FlowMemory\flowmemory-contracts` adds useful settlement object constants, + extra release tests, deployment chain gating, and docs for authority/emergency + assumptions. +- `E:\FlowMemory\flowmemory-bridge-full` expects the existing + `BridgeDeposit(bytes32,uint256,address,address,uint256,bytes32,uint256,bytes32)` + event shape. The relayer derives replay keys and observation IDs from + `sourceChainId`, lockbox address, receipt `txHash`, `logIndex`, and + `depositId`; contracts must not emit or assume receipt locator fields. + +## Implementation Plan + +1. Keep the existing bridge architecture and event ABI unchanged. +2. Port the settlement object vocabulary from the long-loop contracts work. +3. Tighten deployment gating in `DeployBridgeSpine` for local Anvil, Base + Sepolia, and the capped Base `8453` pilot. +4. Require explicit Base `8453` pilot acknowledgement and nonzero total caps for + any Base `8453` configured asset. +5. Add tests for the added object vocabulary, partial release/replay behavior, + and zero release parameters. +6. Update bridge/contract docs with owner, release authority, caps, pause, + replay, emergency assumptions, deployed-address handling, and verification + instructions. +7. Add the root `flowchain:real-value-pilot:contracts` proof wrapper. +8. Run focused tests, contract hardening, local Anvil dry run, Base `8453` + missing-ack rejection, Base `8453` acknowledged dry run, + `npm run flowchain:product-e2e`, and `git diff --check`. + +## Deployed-Address Handling Design + +Deployment addresses must be treated as local operator state until a reviewed +pilot record is intentionally published. The deployment script emits a +non-secret deployment event, and Foundry broadcast artifacts stay local. The +operator should store the selected lockbox and settlement-spine addresses in a +local ignored env file or shell variables such as +`FLOWCHAIN_BASE8453_LOCKBOX_ADDRESS`; public docs should describe how to load +that address, not hardcode it as a blanket endorsement. diff --git a/docs/bridge/FLOWCHAIN_BASE_BRIDGE_POC.md b/docs/bridge/FLOWCHAIN_BASE_BRIDGE_POC.md index 2b63b650..64f92762 100644 --- a/docs/bridge/FLOWCHAIN_BASE_BRIDGE_POC.md +++ b/docs/bridge/FLOWCHAIN_BASE_BRIDGE_POC.md @@ -1,10 +1,11 @@ # FlowChain Base Bridge POC -Status: test-only bridge lane for local and Base Sepolia validation. +Status: test and capped Base `8453` pilot bridge lane. -This bridge POC is designed so a small canary can be reviewed later without -claiming production bridge readiness. It is not audited, not trustless, not a -public bridge, and not approved for broad mainnet use. +This bridge POC is designed so local, Base Sepolia, and tiny capped Base `8453` +pilot activity can be reviewed without claiming broad bridge readiness. It is +not audited, not trustless, not a public bridge, and not approved for broad +mainnet use. ## What Exists @@ -12,7 +13,9 @@ public bridge, and not approved for broad mainnet use. explicit test release authority, pause, allowlisted tokens, per-deposit caps, total caps, deposit records, replay guards, deposit events, and release hooks. - `contracts/FlowChainSettlementSpine.sol`: compact local/test event spine for - bridge and FlowChain object commitments. + bridge and FlowChain object commitments, including stable object constants for + bridge deposits, bridge credits, withdrawal intents, memory objects, and + finality objects. - `tests/bridge/BaseBridgeLockbox.t.sol`: Foundry coverage for token allowlisting, ERC-20 deposits, native deposits, caps, pause behavior, ownership, release, and replay protection. @@ -74,6 +77,34 @@ is the exact file for the runtime/control-plane to consume. - Bridge observations are advisory local objects until the FlowChain runtime verifies and accepts them. +## Authority And Emergency Assumptions + +- Owner: configures token allowlist entries, per-deposit caps, total caps, pause + state, and the release authority. Owner control is a pilot operator model, not + production governance. +- Release authority: can call `releaseNative` and `releaseERC20` for recorded + deposits. It is expected to be a pilot operator or local relayer identity, not + an unaudited public bridge validator set. +- Pause: blocks new deposits only. Releases remain available while paused so an + operator can unwind or recover deposits according to explicit evidence. +- Caps: each allowed asset has a nonzero per-deposit cap. Base `8453` + deployment configuration additionally requires a nonzero per-asset total cap. + Total locked accounting is reduced as releases are recorded. +- Replay protection: deposits include a monotonically increasing lockbox nonce; + releases are keyed by deposit, recipient, token, amount, and evidence hash. + Reusing the same release evidence for the same release details reverts. +- Emergency boundary: a compromised owner or release authority can misuse this + POC. The intended emergency tools are pause, cap reduction above current + locked amount, allowlist disablement, authority rotation, and explicit + release/recovery calls. This is why the lockbox is only suitable for a tiny + capped pilot. +- Native release boundary: `releaseNative` uses Solidity `transfer`; use simple + EOA or plain `receive` recipients for pilot recovery unless a smart-contract + recipient has been separately reviewed. +- Token boundary: use plain ERC-20s for rehearsal and only explicitly approved + assets for the Base `8453` pilot. Fee-on-transfer, rebasing, callback-heavy, + or otherwise nonstandard assets are outside the pilot safety claim. + ## Local Mock ```powershell @@ -157,7 +188,8 @@ Use `-RpcUrl` or `ANVIL_RPC_URL` if the Anvil endpoint is not ## Foundry Deploy Script -The contract-side bridge spine has a dry-run-by-default Foundry script: +The contract-side bridge spine has one dry-run-by-default Foundry script for +the existing lockbox and settlement spine: ```powershell $env:FLOWCHAIN_BRIDGE_OWNER = "0x..." @@ -175,9 +207,87 @@ forge script script/DeployBridgeSpine.s.sol:DeployBridgeSpine ` --rpc-url http://127.0.0.1:8545 ``` -For Base Sepolia dry-run, use `--rpc-url $env:BASE_SEPOLIA_RPC_URL`. Add -`--broadcast` only after the environment values are explicit and the owner key -is intentionally supplied to Foundry. Do not commit RPC URLs or private keys. +For Base Sepolia dry-run, use `--rpc-url $env:BASE_SEPOLIA_RPC_URL`. + +For the capped Base `8453` pilot dry run, set explicit local env values and a +nonzero total cap for every configured asset: + +```powershell +$env:FLOWCHAIN_BASE8453_RPC_URL = "" +$env:FLOWCHAIN_BRIDGE_OWNER = "" +$env:FLOWCHAIN_BRIDGE_RELEASE_AUTHORITY = "" +$env:FLOWCHAIN_SETTLEMENT_SUBMITTER = "" +$env:FLOWCHAIN_BRIDGE_ALLOW_NATIVE = "true" +$env:FLOWCHAIN_BRIDGE_NATIVE_PER_DEPOSIT_CAP = "1000000000000000" +$env:FLOWCHAIN_BRIDGE_NATIVE_TOTAL_CAP = "5000000000000000" +$env:FLOWCHAIN_BRIDGE_ALLOW_ERC20 = "false" +$env:FLOWCHAIN_BRIDGE_ERC20_TOKEN = "0x0000000000000000000000000000000000000000" +$env:FLOWCHAIN_BRIDGE_ERC20_PER_DEPOSIT_CAP = "0" +$env:FLOWCHAIN_BRIDGE_ERC20_TOTAL_CAP = "0" +$env:FLOWCHAIN_BASE8453_PILOT_ACK = "true" + +forge script script/DeployBridgeSpine.s.sol:DeployBridgeSpine ` + --rpc-url $env:FLOWCHAIN_BASE8453_RPC_URL +``` + +The `8453` path reverts unless `FLOWCHAIN_BASE8453_PILOT_ACK=true`. Broadcast is +the same script with `--broadcast`, but the deployer key must come from a local +ignored env var or secure shell secret and must never be committed: + +```powershell +forge script script/DeployBridgeSpine.s.sol:DeployBridgeSpine ` + --rpc-url $env:FLOWCHAIN_BASE8453_RPC_URL ` + --broadcast ` + --private-key $env:FLOWCHAIN_BASE8453_DEPLOYER_PRIVATE_KEY +``` + +The script rejects chains other than local Anvil `31337`, Base Sepolia `84532`, +and Base `8453`. It also rejects Base `8453` configured assets with zero total +cap. Do not commit RPC URLs or private keys. + +## Deployed Address Handling + +Deployment addresses are local operator state until a reviewed pilot record is +intentionally published. Store addresses in an ignored local env file or shell +variables, for example: + +```powershell +$env:FLOWCHAIN_BASE8453_LOCKBOX_ADDRESS = "0x..." +$env:FLOWCHAIN_BASE8453_SETTLEMENT_SPINE_ADDRESS = "0x..." +``` + +Public docs should name how the relayer loads `FLOWCHAIN_BASE8453_LOCKBOX_ADDRESS` +and the deployment block range; they should not hardcode a lockbox address as a +blanket endorsement. Foundry `broadcast/` artifacts are ignored by Git and may +be used as local evidence for the PR summary. + +## Source Verification + +After a reviewed pilot broadcast, verify the two deployed sources with the +constructor arguments used locally: + +```powershell +$env:BASESCAN_API_KEY = "" + +forge verify-contract ` + --chain-id 8453 ` + ` + contracts/bridge/BaseBridgeLockbox.sol:BaseBridgeLockbox ` + --constructor-args $(cast abi-encode "constructor(address,address)" ) ` + --etherscan-api-key $env:BASESCAN_API_KEY ` + --watch + +forge verify-contract ` + --chain-id 8453 ` + ` + contracts/FlowChainSettlementSpine.sol:FlowChainSettlementSpine ` + --constructor-args $(cast abi-encode "constructor(address)" ) ` + --etherscan-api-key $env:BASESCAN_API_KEY ` + --watch +``` + +For dry-run planning, run the same commands with placeholder addresses in the +PR notes and do not submit without the reviewed deployment addresses. ## Contract Event Schema @@ -247,9 +357,12 @@ event FlowChainObjectCommitted( ``` Bridge agents should use `BRIDGE_DEPOSIT_OBJECT` as `objectType` when committing -a FlowChain bridge-deposit object derived from a `BridgeDeposit`. Indexers still -derive `txHash`, `logIndex`, and block metadata from receipts and logs; those -fields are not emitted by the contracts. +a FlowChain bridge-deposit object derived from a `BridgeDeposit`. +`BRIDGE_CREDIT_OBJECT` and `BRIDGE_WITHDRAWAL_INTENT_OBJECT` are the matching +object types for credit and withdrawal-intent commitments. `MEMORY_OBJECT` and +`FINALITY_OBJECT` remain available for control-plane object commitments. +Indexers still derive `txHash`, `logIndex`, and block metadata from receipts and +logs; those fields are not emitted by the contracts. ## Base Mainnet Canary Read @@ -350,6 +463,7 @@ Failure, retry, and replay behavior: ```powershell forge test --match-path tests/bridge/BaseBridgeLockbox.t.sol forge test --match-path tests/FlowChainSettlementSpine.t.sol +npm run flowchain:real-value-pilot:contracts npm run bridge:test npm run bridge:mock npm run bridge:sepolia:observe diff --git a/infra/scripts/flowchain-real-value-pilot-contracts-e2e.ps1 b/infra/scripts/flowchain-real-value-pilot-contracts-e2e.ps1 new file mode 100644 index 00000000..4cc8459d --- /dev/null +++ b/infra/scripts/flowchain-real-value-pilot-contracts-e2e.ps1 @@ -0,0 +1,224 @@ +param( + [switch] $SkipHardening +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +. "$PSScriptRoot\flowchain-common.ps1" + +function Invoke-ContractsProofCommand { + param( + [Parameter(Mandatory = $true)] + [string] $Label, + + [Parameter(Mandatory = $true)] + [string] $FilePath, + + [string[]] $ArgumentList = @() + ) + + [void] $script:commandsRun.Add("$FilePath $(Join-FlowChainProcessArguments -ArgumentList $ArgumentList)".Trim()) + Invoke-FlowChainCommand -Label $Label -FilePath $FilePath -ArgumentList $ArgumentList +} + +function Invoke-CapturedContractsCommand { + param( + [Parameter(Mandatory = $true)] + [string] $Label, + + [Parameter(Mandatory = $true)] + [string] $FilePath, + + [string[]] $ArgumentList = @(), + + [switch] $ExpectFailure, + + [string] $ExpectedText = "" + ) + + $display = "$FilePath $(Join-FlowChainProcessArguments -ArgumentList $ArgumentList)".Trim() + [void] $script:commandsRun.Add($display) + + Write-Host "" + Write-Host "== $Label ==" + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + $output = (& $FilePath @ArgumentList 2>&1) | ForEach-Object { "$_" } + $exitCode = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + + if ($ExpectFailure) { + if ($exitCode -eq 0) { + throw "Expected command to fail, but it passed: $display" + } + } + elseif ($exitCode -ne 0) { + throw "$Label failed with exit code ${exitCode}: $display`n$($output -join [Environment]::NewLine)" + } + + $body = $output -join [Environment]::NewLine + if (-not [string]::IsNullOrWhiteSpace($ExpectedText) -and $body -notlike "*$ExpectedText*") { + throw "$Label did not include expected text: $ExpectedText" + } + + Write-Host $body + return $body +} + +function Set-BridgeDeployProofEnv { + param( + [bool] $PilotAck, + [string] $NativeTotalCap = "5000000000000000" + ) + + $env:FLOWCHAIN_BRIDGE_OWNER = "0x1111111111111111111111111111111111111111" + $env:FLOWCHAIN_BRIDGE_RELEASE_AUTHORITY = "0x2222222222222222222222222222222222222222" + $env:FLOWCHAIN_SETTLEMENT_SUBMITTER = "0x3333333333333333333333333333333333333333" + $env:FLOWCHAIN_BRIDGE_ALLOW_NATIVE = "true" + $env:FLOWCHAIN_BRIDGE_NATIVE_PER_DEPOSIT_CAP = "1000000000000000" + $env:FLOWCHAIN_BRIDGE_NATIVE_TOTAL_CAP = $NativeTotalCap + $env:FLOWCHAIN_BRIDGE_ALLOW_ERC20 = "false" + $env:FLOWCHAIN_BRIDGE_ERC20_TOKEN = "0x0000000000000000000000000000000000000000" + $env:FLOWCHAIN_BRIDGE_ERC20_PER_DEPOSIT_CAP = "0" + $env:FLOWCHAIN_BRIDGE_ERC20_TOTAL_CAP = "0" + if ($PilotAck) { + $env:FLOWCHAIN_BASE8453_PILOT_ACK = "true" + } + else { + $env:FLOWCHAIN_BASE8453_PILOT_ACK = $null + } +} + +$repoRoot = Set-FlowChainRepoRoot +$reportDir = Assert-FlowChainPathInsideRepo -RepoRoot $repoRoot -Path (Resolve-FlowChainPath -RepoRoot $repoRoot -Path "devnet/local/real-value-pilot/contracts-e2e") +if (Test-Path -LiteralPath $reportDir) { + Remove-Item -LiteralPath $reportDir -Recurse -Force +} +New-Item -ItemType Directory -Force -Path $reportDir | Out-Null + +$script:commandsRun = New-Object System.Collections.ArrayList +$envNames = @( + "FLOWCHAIN_BRIDGE_OWNER", + "FLOWCHAIN_BRIDGE_RELEASE_AUTHORITY", + "FLOWCHAIN_SETTLEMENT_SUBMITTER", + "FLOWCHAIN_BRIDGE_ALLOW_NATIVE", + "FLOWCHAIN_BRIDGE_NATIVE_PER_DEPOSIT_CAP", + "FLOWCHAIN_BRIDGE_NATIVE_TOTAL_CAP", + "FLOWCHAIN_BRIDGE_ALLOW_ERC20", + "FLOWCHAIN_BRIDGE_ERC20_TOKEN", + "FLOWCHAIN_BRIDGE_ERC20_PER_DEPOSIT_CAP", + "FLOWCHAIN_BRIDGE_ERC20_TOTAL_CAP", + "FLOWCHAIN_BASE8453_PILOT_ACK" +) +$savedEnv = @{} +foreach ($name in $envNames) { + $savedEnv[$name] = [Environment]::GetEnvironmentVariable($name, "Process") +} + +try { + Invoke-ContractsProofCommand ` + -Label "Bridge lockbox focused tests" ` + -FilePath "forge" ` + -ArgumentList @("test", "--match-path", "tests/bridge/BaseBridgeLockbox.t.sol") + + Invoke-ContractsProofCommand ` + -Label "Settlement spine focused tests" ` + -FilePath "forge" ` + -ArgumentList @("test", "--match-path", "tests/FlowChainSettlementSpine.t.sol") + + if (-not $SkipHardening) { + Invoke-ContractsProofCommand ` + -Label "Contract hardening gate" ` + -FilePath "npm" ` + -ArgumentList @("run", "contracts:hardening") + } + + Set-BridgeDeployProofEnv -PilotAck $false + Invoke-CapturedContractsCommand ` + -Label "Local Anvil bridge-spine dry run" ` + -FilePath "forge" ` + -ArgumentList @("script", "script/DeployBridgeSpine.s.sol:DeployBridgeSpine", "--chain-id", "31337") ` + -ExpectedText "chainId: 31337" | Out-Null + + Invoke-CapturedContractsCommand ` + -Label "Base 8453 dry run rejects missing pilot ack" ` + -FilePath "forge" ` + -ArgumentList @("script", "script/DeployBridgeSpine.s.sol:DeployBridgeSpine", "--chain-id", "8453") ` + -ExpectFailure ` + -ExpectedText "Base8453PilotAckRequired" | Out-Null + + Set-BridgeDeployProofEnv -PilotAck $true + Invoke-CapturedContractsCommand ` + -Label "Base 8453 bridge-spine dry run with pilot ack" ` + -FilePath "forge" ` + -ArgumentList @("script", "script/DeployBridgeSpine.s.sol:DeployBridgeSpine", "--chain-id", "8453") ` + -ExpectedText "chainId: 8453" | Out-Null +} +finally { + foreach ($name in $envNames) { + [Environment]::SetEnvironmentVariable($name, $savedEnv[$name], "Process") + } +} + +$bridgeDoc = Get-Content -Raw -LiteralPath (Join-Path $repoRoot "docs/bridge/FLOWCHAIN_BASE_BRIDGE_POC.md") +foreach ($expected in @( + "FLOWCHAIN_BASE8453_PILOT_ACK", + "forge verify-contract", + "Do not commit RPC URLs or private keys", + "BRIDGE_CREDIT_OBJECT", + "BRIDGE_WITHDRAWAL_INTENT_OBJECT" + )) { + if ($bridgeDoc -notlike "*$expected*") { + throw "Bridge contract documentation is missing expected evidence text: $expected" + } +} + +$deploymentBoundary = Get-Content -Raw -LiteralPath (Join-Path $repoRoot "contracts/DEPLOYMENT_BOUNDARY.md") +foreach ($expected in @("8453", "nonzero total caps", "FLOWCHAIN_BASE8453_PILOT_ACK=true")) { + if ($deploymentBoundary -notlike "*$expected*") { + throw "Deployment boundary documentation is missing expected evidence text: $expected" + } +} + +$reportPath = Join-Path $reportDir "flowchain-real-value-pilot-contracts-e2e-report.json" +$report = [ordered]@{ + schema = "flowchain.real_value_pilot.contracts_e2e_report.v0" + generatedAt = (Get-Date).ToUniversalTime().ToString("o") + status = "passed" + productionReady = $false + commandsRun = @($commandsRun) + checks = [ordered]@{ + bridgeLockboxTests = "passed" + settlementSpineTests = "passed" + contractHardening = $(if ($SkipHardening) { "skipped" } else { "passed" }) + localAnvilDryRun = "passed" + base8453MissingAckRejection = "passed" + base8453PilotAckDryRun = "passed" + documentationEvidence = "passed" + } + evidence = [ordered]@{ + chainIds = @(31337, 84532, 8453) + base8453AckEnv = "FLOWCHAIN_BASE8453_PILOT_ACK=true" + deploymentScript = "script/DeployBridgeSpine.s.sol" + lockboxTests = "tests/bridge/BaseBridgeLockbox.t.sol" + settlementTests = "tests/FlowChainSettlementSpine.t.sol" + deploymentBoundary = "contracts/DEPLOYMENT_BOUNDARY.md" + bridgeDoc = "docs/bridge/FLOWCHAIN_BASE_BRIDGE_POC.md" + } + boundary = @( + "capped owner pilot only", + "dry-run by default", + "no committed private key", + "no broad bridge readiness claim" + ) +} +Write-FlowChainJson -Path $reportPath -Value $report -Depth 12 + +Write-Host "" +Write-Host "FlowChain real-value pilot contracts E2E passed." +Write-Host "Report: $reportPath" diff --git a/package.json b/package.json index b8ea1690..9e837132 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "flowchain:l1-e2e": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-full-smoke.ps1", "flowchain:real-value-pilot:e2e": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-e2e.ps1", "flowchain:real-value-pilot": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot.ps1", + "flowchain:real-value-pilot:contracts": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-contracts-e2e.ps1", "flowchain:real-value-pilot:ops": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-ops-e2e.ps1", "flowchain:real-value-pilot:emergency-stop": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-emergency-stop.ps1", "flowchain:real-value-pilot:export": "powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/flowchain-real-value-pilot-export.ps1", diff --git a/script/DeployBridgeSpine.s.sol b/script/DeployBridgeSpine.s.sol index 0792e9b1..db90f452 100644 --- a/script/DeployBridgeSpine.s.sol +++ b/script/DeployBridgeSpine.s.sol @@ -9,25 +9,32 @@ interface BridgeSpineVm { function stopBroadcast() external; function envAddress(string calldata key) external returns (address value); function envBool(string calldata key) external returns (bool value); + function envOr(string calldata key, bool defaultValue) external returns (bool value); function envUint(string calldata key) external returns (uint256 value); } /// @title DeployBridgeSpine -/// @notice Foundry script for local Anvil and Base Sepolia bridge-spine testing. +/// @notice Foundry script for local Anvil, Base Sepolia, and capped Base 8453 pilot bridge-spine deployment. /// @dev Dry-run with `forge script` by default. Add `--broadcast` only after -/// setting explicit test environment variables. +/// setting explicit environment variables and the Base 8453 pilot ack when applicable. contract DeployBridgeSpine { BridgeSpineVm private constant VM = BridgeSpineVm(address(uint160(uint256(keccak256("hevm cheat code"))))); + address private constant NATIVE_TOKEN = address(0); + uint256 internal constant LOCAL_ANVIL_CHAIN_ID = 31_337; + uint256 internal constant BASE_SEPOLIA_CHAIN_ID = 84_532; + uint256 internal constant BASE_MAINNET_CHAIN_ID = 8_453; struct Deployment { address lockbox; address settlementSpine; + uint256 chainId; address owner; address releaseAuthority; address settlementSubmitter; address erc20Token; bool allowNative; bool allowErc20; + bool base8453PilotAck; } struct Config { @@ -41,27 +48,32 @@ contract DeployBridgeSpine { uint256 nativeTotalCap; uint256 erc20PerDepositCap; uint256 erc20TotalCap; + bool base8453PilotAck; } error Erc20TokenRequired(); + error NoBridgeAssetAllowed(); + error UnsupportedBridgeSpineDeploymentChain(uint256 chainId); + error Base8453PilotAckRequired(); + error PilotTotalCapRequired(address token); event FlowChainBridgeSpineDeployed( address indexed lockbox, address indexed settlementSpine, - address indexed owner, + uint256 indexed chainId, + address owner, address releaseAuthority, address settlementSubmitter, address erc20Token, bool allowNative, - bool allowErc20 + bool allowErc20, + bool base8453PilotAck ); function run() external returns (Deployment memory deployment) { Config memory config = _readConfig(); - - if (config.allowErc20 && config.erc20Token == address(0)) { - revert Erc20TokenRequired(); - } + uint256 chainId = _enforceDeploymentGate(config); + _validateConfig(config, chainId); VM.startBroadcast(config.owner); @@ -69,7 +81,7 @@ contract DeployBridgeSpine { FlowChainSettlementSpine settlementSpine = new FlowChainSettlementSpine(config.owner); if (config.allowNative) { - lockbox.configureToken(address(0), true, config.nativePerDepositCap, config.nativeTotalCap); + lockbox.configureToken(NATIVE_TOKEN, true, config.nativePerDepositCap, config.nativeTotalCap); } if (config.allowErc20) { lockbox.configureToken(config.erc20Token, true, config.erc20PerDepositCap, config.erc20TotalCap); @@ -81,23 +93,27 @@ contract DeployBridgeSpine { deployment = Deployment({ lockbox: address(lockbox), settlementSpine: address(settlementSpine), + chainId: chainId, owner: config.owner, releaseAuthority: config.releaseAuthority, settlementSubmitter: config.settlementSubmitter, erc20Token: config.erc20Token, allowNative: config.allowNative, - allowErc20: config.allowErc20 + allowErc20: config.allowErc20, + base8453PilotAck: config.base8453PilotAck }); emit FlowChainBridgeSpineDeployed( address(lockbox), address(settlementSpine), + chainId, config.owner, config.releaseAuthority, config.settlementSubmitter, config.erc20Token, config.allowNative, - config.allowErc20 + config.allowErc20, + config.base8453PilotAck ); VM.stopBroadcast(); @@ -114,7 +130,35 @@ contract DeployBridgeSpine { nativePerDepositCap: VM.envUint("FLOWCHAIN_BRIDGE_NATIVE_PER_DEPOSIT_CAP"), nativeTotalCap: VM.envUint("FLOWCHAIN_BRIDGE_NATIVE_TOTAL_CAP"), erc20PerDepositCap: VM.envUint("FLOWCHAIN_BRIDGE_ERC20_PER_DEPOSIT_CAP"), - erc20TotalCap: VM.envUint("FLOWCHAIN_BRIDGE_ERC20_TOTAL_CAP") + erc20TotalCap: VM.envUint("FLOWCHAIN_BRIDGE_ERC20_TOTAL_CAP"), + base8453PilotAck: VM.envOr("FLOWCHAIN_BASE8453_PILOT_ACK", false) }); } + + function _enforceDeploymentGate(Config memory config) private view returns (uint256 chainId) { + chainId = block.chainid; + if (chainId != LOCAL_ANVIL_CHAIN_ID && chainId != BASE_SEPOLIA_CHAIN_ID && chainId != BASE_MAINNET_CHAIN_ID) { + revert UnsupportedBridgeSpineDeploymentChain(chainId); + } + if (chainId == BASE_MAINNET_CHAIN_ID && !config.base8453PilotAck) { + revert Base8453PilotAckRequired(); + } + } + + function _validateConfig(Config memory config, uint256 chainId) private pure { + if (!config.allowNative && !config.allowErc20) { + revert NoBridgeAssetAllowed(); + } + if (config.allowErc20 && config.erc20Token == address(0)) { + revert Erc20TokenRequired(); + } + if (chainId == BASE_MAINNET_CHAIN_ID) { + if (config.allowNative && config.nativeTotalCap == 0) { + revert PilotTotalCapRequired(NATIVE_TOKEN); + } + if (config.allowErc20 && config.erc20TotalCap == 0) { + revert PilotTotalCapRequired(config.erc20Token); + } + } + } } diff --git a/tests/FlowChainSettlementSpine.t.sol b/tests/FlowChainSettlementSpine.t.sol index 39e38ac7..e2a46c00 100644 --- a/tests/FlowChainSettlementSpine.t.sol +++ b/tests/FlowChainSettlementSpine.t.sol @@ -97,10 +97,59 @@ contract FlowChainSettlementSpineTest { _assertTrue(record.exists); _assertObjectCommittedLog( - logs[logs.length - 1], objectId, rootfieldId, objectType, commitment, parentObjectId, sequence + logs[logs.length - 1], + objectId, + rootfieldId, + objectType, + address(this), + commitment, + parentObjectId, + sequence, + "bridge://evidence/1" ); } + function testCommitBridgeAndControlPlaneObjectVocabularyWithStableEventShape() public { + bytes32 rootfieldId = keccak256("rootfield.flowchain.objects"); + + _commitKnownObjectType({ + objectType: spine.BRIDGE_CREDIT_OBJECT(), + objectId: keccak256("bridge.credit.1"), + rootfieldId: rootfieldId, + commitment: keccak256("bridge.credit.commitment"), + parentObjectId: keccak256("bridge.deposit.1"), + evidenceURI: "bridge://credit/1", + expectedSequence: 1 + }); + _commitKnownObjectType({ + objectType: spine.BRIDGE_WITHDRAWAL_INTENT_OBJECT(), + objectId: keccak256("bridge.withdrawal.intent.1"), + rootfieldId: rootfieldId, + commitment: keccak256("bridge.withdrawal.intent.commitment"), + parentObjectId: keccak256("bridge.credit.1"), + evidenceURI: "bridge://withdrawal-intent/1", + expectedSequence: 2 + }); + _commitKnownObjectType({ + objectType: spine.MEMORY_OBJECT(), + objectId: keccak256("memory.cell.1"), + rootfieldId: rootfieldId, + commitment: keccak256("memory.cell.commitment"), + parentObjectId: bytes32(0), + evidenceURI: "memory://cell/1", + expectedSequence: 3 + }); + _commitKnownObjectType({ + objectType: spine.FINALITY_OBJECT(), + objectId: keccak256("finality.receipt.1"), + rootfieldId: rootfieldId, + commitment: keccak256("finality.receipt.commitment"), + parentObjectId: keccak256("memory.cell.1"), + evidenceURI: "finality://receipt/1", + expectedSequence: 4 + }); + } + function testAuthorizedSubmitterCanCommitAndRevocationBlocksFutureCommits() public { bytes32 bridgeDepositObject = spine.BRIDGE_DEPOSIT_OBJECT(); bytes32 objectId = keccak256("bridge.deposit.authorized"); @@ -222,9 +271,11 @@ contract FlowChainSettlementSpineTest { bytes32 objectId, bytes32 rootfieldId, bytes32 objectType, + address expectedSubmitter, bytes32 commitment, bytes32 parentObjectId, - uint64 sequence + uint64 sequence, + string memory expectedEvidenceURI ) private view { _assertTrue(log.emitter == address(spine)); _assertTrue(log.topics[0] == OBJECT_COMMITTED_SIGNATURE); @@ -241,12 +292,52 @@ contract FlowChainSettlementSpineTest { string memory evidenceURI ) = abi.decode(log.data, (address, bytes32, bytes32, uint64, uint64, string)); - _assertTrue(decodedSubmitter == address(this)); + _assertTrue(decodedSubmitter == expectedSubmitter); _assertTrue(decodedCommitment == commitment); _assertTrue(decodedParentObjectId == parentObjectId); _assertTrue(decodedSequence == sequence); _assertTrue(committedAt > 0); - _assertTrue(keccak256(bytes(evidenceURI)) == keccak256("bridge://evidence/1")); + _assertTrue(keccak256(bytes(evidenceURI)) == keccak256(bytes(expectedEvidenceURI))); + } + + function _commitKnownObjectType( + bytes32 objectType, + bytes32 objectId, + bytes32 rootfieldId, + bytes32 commitment, + bytes32 parentObjectId, + string memory evidenceURI, + uint64 expectedSequence + ) private { + vm.recordLogs(); + uint64 sequence = spine.commitObject(objectType, objectId, rootfieldId, commitment, parentObjectId, evidenceURI); + SettlementVm.Log[] memory logs = vm.getRecordedLogs(); + + _assertTrue(sequence == expectedSequence); + _assertTrue(spine.nextSequence() == expectedSequence + 1); + _assertTrue(spine.isObjectCommitted(objectId)); + + FlowChainSettlementSpine.ObjectCommitment memory record = spine.getObjectCommitment(objectId); + _assertTrue(record.submitter == address(this)); + _assertTrue(record.objectType == objectType); + _assertTrue(record.rootfieldId == rootfieldId); + _assertTrue(record.commitment == commitment); + _assertTrue(record.parentObjectId == parentObjectId); + _assertTrue(record.sequence == expectedSequence); + _assertTrue(record.committedAt > 0); + _assertTrue(record.exists); + + _assertObjectCommittedLog( + logs[logs.length - 1], + objectId, + rootfieldId, + objectType, + address(this), + commitment, + parentObjectId, + sequence, + evidenceURI + ); } function _assertTrue(bool value) private pure { diff --git a/tests/bridge/BaseBridgeLockbox.t.sol b/tests/bridge/BaseBridgeLockbox.t.sol index a9a5842c..8dd87302 100644 --- a/tests/bridge/BaseBridgeLockbox.t.sol +++ b/tests/bridge/BaseBridgeLockbox.t.sol @@ -326,6 +326,41 @@ contract BaseBridgeLockboxTest { lockbox.releaseERC20(depositId, address(caller), address(token), 1 ether, EVIDENCE_HASH); } + function testReleaseERC20CanReleaseInPartsWithDistinctEvidenceUntilExhausted() public { + bytes32 depositId = caller.lockERC20(lockbox, address(token), 10 ether, RECIPIENT); + bytes32 firstEvidenceHash = keccak256("flowchain.local.release.1"); + bytes32 secondEvidenceHash = keccak256("flowchain.local.release.2"); + + bytes32 firstRelease = + lockbox.releaseERC20(depositId, address(caller), address(token), 4 ether, firstEvidenceHash); + bytes32 secondRelease = + lockbox.releaseERC20(depositId, address(caller), address(token), 6 ether, secondEvidenceHash); + + _assertTrue(firstRelease != secondRelease); + _assertTrue(lockbox.releases(firstRelease)); + _assertTrue(lockbox.releases(secondRelease)); + _assertTrue(lockbox.remainingDepositAmount(depositId) == 0); + _assertTrue(token.balanceOf(address(caller)) == 1_000 ether); + + (,,, uint256 totalLocked) = lockbox.tokenConfigs(address(token)); + _assertTrue(totalLocked == 0); + + vm.expectRevert( + abi.encodeWithSelector(BaseBridgeLockbox.ReleaseAmountExceeded.selector, depositId, 1 wei, 0) + ); + lockbox.releaseERC20(depositId, address(caller), address(token), 1 wei, keccak256("flowchain.local.release.3")); + } + + function testReleaseRejectsZeroRecipientAndZeroAmount() public { + bytes32 depositId = caller.lockERC20(lockbox, address(token), 10 ether, RECIPIENT); + + vm.expectRevert(BaseBridgeLockbox.ZeroRecipient.selector); + lockbox.releaseERC20(depositId, address(0), address(token), 1 ether, EVIDENCE_HASH); + + vm.expectRevert(BaseBridgeLockbox.ZeroAmount.selector); + lockbox.releaseERC20(depositId, address(caller), address(token), 0, EVIDENCE_HASH); + } + function testReleaseBlocksTokenMismatchOverReleaseAndZeroEvidence() public { bytes32 depositId = caller.lockERC20(lockbox, address(token), 10 ether, RECIPIENT);