Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 66 additions & 5 deletions docs/bridge/FLOWCHAIN_BASE_BRIDGE_POC.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,24 @@ public bridge, and not approved for broad mainnet use.
- `tests/bridge/BaseBridgeLockbox.t.sol`: Foundry coverage for token
allowlisting, ERC-20 deposits, native deposits, caps, pause behavior,
ownership, release, and replay protection.
- `services/bridge-relayer/`: fixture-first observer that converts explicit
bridge deposit records into FlowChain bridge observation JSON.
- `services/bridge-relayer/`: fixture-first and RPC-range observer that
converts explicit bridge deposit records into FlowChain bridge observation,
credit, withdrawal-intent, and runtime handoff JSON.
- `fixtures/bridge/base-sepolia-mock-deposit.json`: deterministic test deposit.
- `fixtures/bridge/local-runtime-bridge-handoff.json`: deterministic local
bridge handoff consumed by the runtime/control-plane agent until a direct
intake endpoint is merged.
- `schemas/flowmemory/bridge-deposit.schema.json` and
`schemas/flowmemory/bridge-observation.schema.json`: bridge object contracts.
- `schemas/flowmemory/bridge-credit.schema.json`,
`schemas/flowmemory/bridge-withdrawal-intent.schema.json`, and
`schemas/flowmemory/bridge-runtime-handoff.schema.json`: canonical local
credit, test withdrawal-intent, and handoff contracts.
- `infra/scripts/bridge-base-sepolia-observe.ps1`: env-friendly Base Sepolia
observation wrapper that requires no private key.
- `infra/scripts/bridge-base-sepolia-smoke.ps1`: guarded Base Sepolia smoke.
- `infra/scripts/bridge-local-anvil-observe.ps1`: local Anvil observation
wrapper for chain id `31337`.
- `infra/scripts/bridge-base-mainnet-canary-read.ps1`: disabled-by-default
Base mainnet canary read wrapper.

Expand All @@ -30,14 +42,25 @@ Base Sepolia user/test wallet
-> BaseBridgeLockbox.lockERC20 or lockNative
-> BridgeDeposit event
-> bridge-relayer explicit reader/mock observer
-> FlowChain bridge deposit observation fixture
-> local control plane / workbench / devnet handoff
-> BridgeObservation with replay key
-> BridgeCredit pending/applied local object
-> local runtime/control-plane/workbench handoff
```

The POC does not mint production assets on FlowChain. Local acceptance is a
fixture/control-plane event until the private/local runtime explicitly consumes
bridge deposit objects.

The handoff includes a workbench-ready timeline:

```text
deposit observed -> credit pending -> credit applied -> withdrawal requested
```

The current workbench/control-plane packages are outside this bridge-agent
scope. Until their bridge intake lands, `fixtures/bridge/local-runtime-bridge-handoff.json`
is the exact file for the runtime/control-plane agent to consume.

## Risk Model

- Base mainnet uses real funds. Mainnet canary reads require
Expand All @@ -56,12 +79,16 @@ bridge deposit objects.
npm install
npm run bridge:mock
npm run bridge:test
npm run bridge:local-credit:smoke
```

Expected output:

```text
services/bridge-relayer/out/bridge-observation.json
services/bridge-relayer/out/bridge-credit.json
services/bridge-relayer/out/bridge-runtime-handoff.json
fixtures/bridge/local-runtime-bridge-handoff.json
```

## Base Sepolia Smoke
Expand All @@ -79,6 +106,36 @@ powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/bridge-base-se
The script checks Base Sepolia chain id `84532`, requires an explicit lockbox,
requires an explicit block range, and writes a local observation output.

The root package also exposes an env-var smoke path that does not require a
private key:

```powershell
$env:BASE_SEPOLIA_RPC_URL="<base-sepolia-rpc-url>"
$env:BASE_BRIDGE_LOCKBOX_ADDRESS="<deployed-lockbox>"
$env:BASE_BRIDGE_FROM_BLOCK="<from>"
$env:BASE_BRIDGE_TO_BLOCK="<to>"
npm run bridge:sepolia:observe
```

This command reads only `BridgeDeposit` logs from the explicit lockbox and
range, then writes observation, credit, and handoff JSON under
`services/bridge-relayer/out/`.

## Local Anvil Observation

Local Anvil is supported as a mock Base event lane with chain id `31337`.
Deploy `BaseBridgeLockbox`, emit one or more deposits, then run:

```powershell
$env:ANVIL_BRIDGE_LOCKBOX_ADDRESS="<deployed-lockbox>"
$env:ANVIL_BRIDGE_FROM_BLOCK="<from>"
$env:ANVIL_BRIDGE_TO_BLOCK="<to>"
npm run bridge:anvil:observe
```

Use `-RpcUrl` or `ANVIL_RPC_URL` if the Anvil endpoint is not
`http://127.0.0.1:8545`.

## Base Mainnet Canary Read

Only after review, and only for a tiny capped canary:
Expand All @@ -94,14 +151,18 @@ powershell -NoProfile -ExecutionPolicy Bypass -File infra/scripts/bridge-base-ma
```

The script checks Base mainnet chain id `8453` and refuses a canary above
`25` USD.
`25` USD. It is read-only and prints the chain, lockbox, block range, max USD
guardrail, and broadcast status before it reads logs.

## Commands

```powershell
forge test --match-path tests/bridge/BaseBridgeLockbox.t.sol
npm run bridge:test
npm run bridge:mock
npm run bridge:sepolia:observe
npm run bridge:local-credit:smoke
npm run flowchain:full-smoke
git diff --check
```

Expand Down
213 changes: 213 additions & 0 deletions fixtures/bridge/local-runtime-bridge-handoff.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
{
"schema": "flowmemory.bridge_runtime_handoff.v0",
"handoffId": "0xb8f818f1c45a864a7134b298b993952edda161824a4120c7716ce950fe63a2ca",
"generatedAt": "2026-05-13T00:00:00.000Z",
"mode": "mock",
"productionReady": false,
"localOnly": true,
"observations": [
{
"schema": "flowmemory.bridge_deposit_observation.v0",
"observationId": "0x0430f0f7818add19ccd9037dcf6e50d75c1fb0fac0441f9b042c473d1d2d223c",
"replayKey": "0x9c97eb0fa65cb3eec9274cb0c9e925351608e7abe6980fe2525820048bd81e09",
"observedAt": "2026-05-13T00:00:00.000Z",
"mode": "mock",
"productionReady": false,
"deposit": {
"schema": "flowmemory.bridge_deposit.v0",
"depositId": "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269",
"sourceChainId": 84532,
"sourceContract": "0x1111111111111111111111111111111111111111",
"txHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
"logIndex": 0,
"token": "0x3333333333333333333333333333333333333333",
"amount": "20000000",
"sender": "0x4444444444444444444444444444444444444444",
"flowchainRecipient": "0x5555555555555555555555555555555555555555555555555555555555555555",
"nonce": "1",
"metadataHash": "0x6666666666666666666666666666666666666666666666666666666666666666",
"status": "observed"
},
"guardrails": {
"explicitChainId": true,
"explicitContract": true,
"explicitBlockRange": false,
"noSecrets": true
}
}
],
"credits": [
{
"schema": "flowmemory.bridge_credit.v0",
"creditId": "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6",
"observationId": "0x0430f0f7818add19ccd9037dcf6e50d75c1fb0fac0441f9b042c473d1d2d223c",
"depositId": "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269",
"replayKey": "0x9c97eb0fa65cb3eec9274cb0c9e925351608e7abe6980fe2525820048bd81e09",
"source": {
"chainId": 84532,
"contract": "0x1111111111111111111111111111111111111111",
"txHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
"logIndex": 0
},
"token": "0x3333333333333333333333333333333333333333",
"amount": "20000000",
"flowchainRecipient": "0x5555555555555555555555555555555555555555555555555555555555555555",
"status": "applied",
"appliedAt": "2026-05-13T00:00:00.000Z",
"localOnly": true,
"productionReady": false
}
],
"withdrawalIntents": [
{
"schema": "flowmemory.bridge_withdrawal_intent.v0",
"withdrawalIntentId": "0xe6f0da66dc9659e427640f119b24a83b01ccb2f79c745d6d4c28570c5e5e1751",
"creditId": "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6",
"depositId": "0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269",
"sourceChainId": 84532,
"destinationChainId": 84532,
"token": "0x3333333333333333333333333333333333333333",
"amount": "20000000",
"flowchainAccount": "0x5555555555555555555555555555555555555555555555555555555555555555",
"baseRecipient": "0x4444444444444444444444444444444444444444",
"status": "requested",
"requestedAt": "2026-05-13T00:00:00.000Z",
"testMode": true,
"broadcast": false,
"releasePolicy": "test_record_only",
"productionReady": false
}
],
"replayProtection": {
"strategy": "source-chain-contract-tx-log-deposit",
"replayKeys": [
"0x9c97eb0fa65cb3eec9274cb0c9e925351608e7abe6980fe2525820048bd81e09"
],
"duplicateReplayKeys": []
},
"runtimeIntake": {
"status": "handoff_file",
"consumer": "flowchain-runtime-agent",
"expectedPath": "fixtures/bridge/local-runtime-bridge-handoff.json",
"note": "Runtime/control-plane bridge intake is not merged in this scope. Consume this file as the deterministic bridge credit handoff."
},
"workbenchTimeline": [
{
"phase": "deposit_observed",
"status": "observed",
"objectId": "0x0430f0f7818add19ccd9037dcf6e50d75c1fb0fac0441f9b042c473d1d2d223c",
"title": "Deposit observed",
"summary": "Observed lockbox deposit 0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269 on chain 84532."
},
{
"phase": "credit_pending",
"status": "pending",
"objectId": "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6",
"title": "Credit pending",
"summary": "20000000 test units queued for 0x5555555555555555555555555555555555555555555555555555555555555555."
},
{
"phase": "credit_applied",
"status": "applied",
"objectId": "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6",
"title": "Credit applied",
"summary": "20000000 test units applied in local bridge smoke state."
},
{
"phase": "withdrawal_requested",
"status": "requested",
"objectId": "0xe6f0da66dc9659e427640f119b24a83b01ccb2f79c745d6d4c28570c5e5e1751",
"title": "Withdrawal requested",
"summary": "Test-mode local-to-Base withdrawal intent recorded with no broadcast or real release."
}
],
"workbenchRecords": [
{
"sectionKey": "transactions",
"id": "0x0430f0f7818add19ccd9037dcf6e50d75c1fb0fac0441f9b042c473d1d2d223c",
"kind": "Bridge deposit observation",
"title": "0x2222222222222222222222222222222222222222222222222222222222222222",
"summary": "Deposit 0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269 observed from mock.",
"status": "observed",
"facts": [
{
"label": "chain",
"value": "84532"
},
{
"label": "lockbox",
"value": "0x1111111111111111111111111111111111111111"
},
{
"label": "log index",
"value": "0"
},
{
"label": "amount",
"value": "20000000"
}
],
"rawRef": "0x0430f0f7818add19ccd9037dcf6e50d75c1fb0fac0441f9b042c473d1d2d223c"
},
{
"sectionKey": "receipts",
"id": "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6",
"kind": "Bridge credit",
"title": "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6",
"summary": "Credit applied for deposit 0x7e3a7f7ab7dc9b07d762c1f2fce315cf0c08f1a7e854b4dbcb2359efcb9cb269.",
"status": "verified",
"facts": [
{
"label": "recipient",
"value": "0x5555555555555555555555555555555555555555555555555555555555555555"
},
{
"label": "amount",
"value": "20000000"
},
{
"label": "token",
"value": "0x3333333333333333333333333333333333333333"
},
{
"label": "replay key",
"value": "0x9c97eb0fa65cb3eec9274cb0c9e925351608e7abe6980fe2525820048bd81e09"
}
],
"rawRef": "0xff3efb8221533cfc836bffbcee10bdd2d7d4a5615efce9516574245a3b7d74a6"
},
{
"sectionKey": "transactions",
"id": "0xe6f0da66dc9659e427640f119b24a83b01ccb2f79c745d6d4c28570c5e5e1751",
"kind": "Bridge withdrawal intent",
"title": "0xe6f0da66dc9659e427640f119b24a83b01ccb2f79c745d6d4c28570c5e5e1751",
"summary": "Test-mode withdrawal intent recorded; no mainnet or real-funds release is broadcast.",
"status": "pending",
"facts": [
{
"label": "base recipient",
"value": "0x4444444444444444444444444444444444444444"
},
{
"label": "amount",
"value": "20000000"
},
{
"label": "broadcast",
"value": "false"
},
{
"label": "policy",
"value": "test_record_only"
}
],
"rawRef": "0xe6f0da66dc9659e427640f119b24a83b01ccb2f79c745d6d4c28570c5e5e1751"
}
],
"limitations": [
"Bridge objects are for mock, local Anvil, and Base Sepolia test validation by default.",
"No production bridge readiness, audited security, or trustless finality is claimed.",
"Withdrawal intents are test-mode records only and do not broadcast releases.",
"RPC URLs and private keys are never written to bridge artifacts."
]
}
19 changes: 16 additions & 3 deletions infra/scripts/bridge-base-mainnet-canary-read.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,25 @@ param(
[ValidateRange(0.01, 25)]
[double]$MaxUsd,

[string]$Out = "services/bridge-relayer/out/base-mainnet-canary-bridge-observation.json"
[string]$Out = "services/bridge-relayer/out/base-mainnet-canary-bridge-observation.json",

[string]$CreditOut = "services/bridge-relayer/out/base-mainnet-canary-bridge-credit.json",

[string]$HandoffOut = "services/bridge-relayer/out/base-mainnet-canary-bridge-handoff.json"
)

$ErrorActionPreference = "Stop"

$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
Set-Location -LiteralPath $repoRoot

Write-Host "Reading Base mainnet bridge canary logs." -ForegroundColor Yellow
Write-Host "Chain: Base mainnet (8453)"
Write-Host "Lockbox: $LockboxAddress"
Write-Host "Block range: $FromBlock-$ToBlock"
Write-Host "Max USD guardrail: $MaxUsd"
Write-Host "Broadcast: false; this command is read-only."

npm run bridge:observe -- `
--mode base-mainnet-canary `
--rpc-url $RpcUrl `
Expand All @@ -34,6 +45,8 @@ npm run bridge:observe -- `
--to-block $ToBlock `
--acknowledge-real-funds `
--max-usd $MaxUsd `
--out $Out
--out $Out `
--credit-out $CreditOut `
--handoff-out $HandoffOut

Write-Host "Base mainnet canary bridge read wrote $Out" -ForegroundColor Green
Write-Host "Base mainnet canary bridge read wrote $Out and $HandoffOut" -ForegroundColor Green
Loading
Loading