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
46 changes: 37 additions & 9 deletions contracts/bridge/BaseBridgeLockbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ contract BaseBridgeLockbox {
uint256 perDepositCap;
uint256 totalCap;
uint256 totalLocked;
uint256 totalDeposited;
}

struct DepositRecord {
Expand All @@ -32,10 +33,12 @@ contract BaseBridgeLockbox {
address public constant NATIVE_TOKEN = address(0);
bytes32 public constant BRIDGE_DEPOSIT_SCHEMA_ID = keccak256("flowmemory.bridge.deposit.v0");
bytes32 public constant BRIDGE_RELEASE_SCHEMA_ID = keccak256("flowmemory.bridge.release.v0");
bytes32 public constant PILOT_MODE_TAG = keccak256("flowchain.base8453.owner-pilot.v0");

address public owner;
address public releaseAuthority;
bool public paused;
bool public emergencyStopped;
uint256 public nextNonce = 1;

mapping(address token => TokenConfig config) public tokenConfigs;
Expand All @@ -48,6 +51,7 @@ contract BaseBridgeLockbox {
error NotOwner(address caller);
error NotReleaseAuthority(address caller);
error Paused();
error EmergencyStopped();
error ReentrantCall();
error ZeroOwner();
error ZeroReleaseAuthority();
Expand All @@ -68,16 +72,19 @@ contract BaseBridgeLockbox {
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event ReleaseAuthoritySet(address indexed previousAuthority, address indexed newAuthority);
event PausedSet(bool paused);
event EmergencyStopSet(bool stopped);
event TokenConfigured(address indexed token, bool allowed, uint256 perDepositCap, uint256 totalCap);
event BridgeDeposit(
bytes32 indexed depositId,
uint256 indexed sourceChainId,
address indexed sender,
address lockbox,
address token,
uint256 amount,
bytes32 flowchainRecipient,
uint256 nonce,
bytes32 metadataHash
bytes32 metadataHash,
bytes32 pilotModeTag
);
event BridgeRelease(
bytes32 indexed releaseId,
Expand Down Expand Up @@ -109,6 +116,13 @@ contract BaseBridgeLockbox {
_;
}

modifier whenNotEmergencyStopped() {
if (emergencyStopped) {
revert EmergencyStopped();
}
_;
}

modifier nonReentrant() {
if (_entered) {
revert ReentrantCall();
Expand Down Expand Up @@ -158,13 +172,18 @@ contract BaseBridgeLockbox {
emit PausedSet(value);
}

function setEmergencyStopped(bool value) external onlyOwner {
emergencyStopped = value;
emit EmergencyStopSet(value);
}

function configureToken(address token, bool allowed, uint256 perDepositCap, uint256 totalCap) external onlyOwner {
TokenConfig storage config = tokenConfigs[token];
if (allowed && perDepositCap == 0) {
revert ZeroAmount();
}
if (allowed && totalCap != 0 && totalCap < config.totalLocked) {
revert TotalCapExceeded(token, config.totalLocked, totalCap);
if (allowed && totalCap != 0 && totalCap < config.totalDeposited) {
revert TotalCapExceeded(token, config.totalDeposited, totalCap);
}

config.allowed = allowed;
Expand All @@ -177,6 +196,7 @@ contract BaseBridgeLockbox {
external
payable
whenNotPaused
whenNotEmergencyStopped
nonReentrant
returns (bytes32 depositId)
{
Expand All @@ -186,6 +206,7 @@ contract BaseBridgeLockbox {
function lockERC20(address token, uint256 amount, bytes32 flowchainRecipient, bytes32 metadataHash)
external
whenNotPaused
whenNotEmergencyStopped
nonReentrant
returns (bytes32 depositId)
{
Expand All @@ -201,6 +222,7 @@ contract BaseBridgeLockbox {
function releaseNative(bytes32 depositId, address payable recipient, uint256 amount, bytes32 evidenceHash)
external
onlyReleaseAuthority
whenNotEmergencyStopped
nonReentrant
returns (bytes32 releaseId)
{
Expand All @@ -214,6 +236,7 @@ contract BaseBridgeLockbox {
function releaseERC20(bytes32 depositId, address recipient, address token, uint256 amount, bytes32 evidenceHash)
external
onlyReleaseAuthority
whenNotEmergencyStopped
nonReentrant
returns (bytes32 releaseId)
{
Expand Down Expand Up @@ -257,9 +280,10 @@ contract BaseBridgeLockbox {
revert PerDepositCapExceeded(token, amount, config.perDepositCap);
}

uint256 nextTotal = config.totalLocked + amount;
if (config.totalCap != 0 && nextTotal > config.totalCap) {
revert TotalCapExceeded(token, nextTotal, config.totalCap);
uint256 nextTotalLocked = config.totalLocked + amount;
uint256 nextTotalDeposited = config.totalDeposited + amount;
if (config.totalCap != 0 && nextTotalDeposited > config.totalCap) {
revert TotalCapExceeded(token, nextTotalDeposited, config.totalCap);
}

uint256 nonce = nextNonce++;
Expand All @@ -273,7 +297,8 @@ contract BaseBridgeLockbox {
amount,
flowchainRecipient,
nonce,
metadataHash
metadataHash,
PILOT_MODE_TAG
)
);
if (deposits[depositId]) {
Expand All @@ -291,17 +316,20 @@ contract BaseBridgeLockbox {
metadataHash: metadataHash,
exists: true
});
config.totalLocked = nextTotal;
config.totalLocked = nextTotalLocked;
config.totalDeposited = nextTotalDeposited;

emit BridgeDeposit({
depositId: depositId,
sourceChainId: block.chainid,
sender: sender,
lockbox: address(this),
token: token,
amount: amount,
flowchainRecipient: flowchainRecipient,
nonce: nonce,
metadataHash: metadataHash
metadataHash: metadataHash,
pilotModeTag: PILOT_MODE_TAG
});
}

Expand Down
28 changes: 28 additions & 0 deletions docs/agent-runs/production-l1-bridge/ASSET_DECISION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Asset Decision

Status: implemented.

The lockbox supports both native Base ETH and ERC20 custody paths. The owner pilot activates exactly one supported asset through configuration:

- `FLOWCHAIN_BASE8453_SUPPORTED_TOKEN=0x0000000000000000000000000000000000000000` means native ETH.
- `FLOWCHAIN_BASE8453_SUPPORTED_TOKEN=<erc20-address>` means the allowlisted ERC20.

Why this decision fits the current architecture:

- `BaseBridgeLockbox.lockNative` already handles native deposits with `msg.value`.
- `BaseBridgeLockbox.lockERC20` already handles ERC20 deposits with allowance and `transferFrom`.
- `configureToken` stores per-asset allowlist status, per-deposit cap, total pilot cap, and cumulative deposited amount.
- Release uses separate paths: `releaseNative` and `releaseERC20`.

Refusal behavior:

- Live observation refuses deposits for tokens not listed through `--supported-token` or `FLOWCHAIN_BASE8453_SUPPORTED_TOKEN`.
- ERC20 deposits fail if the token is not allowlisted, amount is zero, allowance is missing, transfer fails, per-deposit cap is exceeded, or total cap is exceeded.
- Native deposits fail if native asset is not allowlisted, `msg.value` is zero, caps are exceeded, deposits are paused, or emergency stop is active.

Proof:

- `forge test --match-path tests/bridge/BaseBridgeLockbox.t.sol` passed.
- `forge test` passed.
- Mock pilot evidence used ERC20 token `0x3333333333333333333333333333333333333333`.
- Native ETH support remains contract-tested and live-gated by choosing the zero address as the supported token.
86 changes: 86 additions & 0 deletions docs/agent-runs/production-l1-bridge/CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Capped Base 8453 Bridge Pilot Checklist

## Context

- [x] `AGENTS.md`
- [x] `docs/START_HERE.md`
- [x] `docs/FLOWMEMORY_HQ_CONTEXT.md`
- [x] `docs/CURRENT_STATE.md`
- [x] `docs/ROOTFLOW_V0.md`
- [x] `docs/FLOW_MEMORY_V0.md`
- [x] `docs/V0_LAUNCH_ACCEPTANCE.md`
- [x] Bridge POC docs and existing handoffs

## Contract

- [x] Base chain ID `8453` deployment configuration
- [x] Asset allowlist or documented native-only decision
- [x] Per-deposit cap
- [x] Total pilot cap
- [x] Deposit pause
- [x] Emergency stop
- [x] Owner authority
- [x] Release authority separation where possible
- [x] Replay protection for deposits and releases
- [x] Deterministic relayer event fields
- [x] Contract tests for happy path and refusal paths

## Relayer

- [x] `eth_chainId == 0x2105` live gate
- [x] Operator acknowledgement live gate
- [x] Required env validation without secret logging
- [x] Bounded block scan
- [x] Confirmation-depth checks
- [x] Deterministic observation ID
- [x] Deterministic credit ID
- [x] Evidence JSON export
- [x] Credit exactly once
- [x] Duplicate/replay handling
- [x] Withdrawal intent
- [x] Release evidence
- [x] Secret-free evidence bundle export

## Ops

- [x] Dry-run deploy command
- [x] Broadcast deploy command with acknowledgement
- [x] Observe command
- [x] Credit command
- [x] Replay check command
- [x] Withdrawal intent command
- [x] Release evidence command
- [x] Pause command
- [x] Resume command
- [x] Emergency stop command
- [x] Evidence export command
- [x] Mock pilot E2E command
- [x] Live readiness check command

## Proof

- [x] `CONTRACT_PROOF.md`
- [x] `RELAYER_PROOF.md`
- [x] `MOCK_PILOT_E2E_PROOF.md`
- [x] `REPLAY_PROOF.md`
- [x] `LIVE_READINESS_PROOF.md`
- [x] `OWNER_LIVE_TEST_COMMANDS.md`
- [x] `ASSET_DECISION.md`
- [x] `DEPLOYMENT_READINESS_PROOF.md`
- [x] `DEPOSIT_EVENT_PROOF.md`
- [x] `LIVE_OBSERVATION_GATE_PROOF.md`
- [x] `CREDIT_APPLICATION_PROOF.md`
- [x] `REAL_FUNDS_PILOT_RUNBOOK.md`
- [x] `WITHDRAW_RELEASE_PROOF.md`
- [x] `FULL_MOCK_PILOT_PROOF.md`
- [x] `HANDOFF.md`

## Verification

- [x] `forge test`
- [x] `npm test --prefix services/bridge-relayer`
- [x] `npm run bridge:local-credit:smoke`
- [x] `npm run bridge:pilot:mock:e2e`
- [x] `npm run bridge:pilot:live:check`
- [x] `npm run flowchain:real-value-pilot:e2e`
- [x] `git diff --check`
56 changes: 56 additions & 0 deletions docs/agent-runs/production-l1-bridge/CONTRACT_PROOF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Contract Proof

Status: implemented and tested.

Changed contract:

- `contracts/bridge/BaseBridgeLockbox.sol`

Core behavior proved:

- Base deployment configuration is guarded for chain ID `8453` through deploy and observer scripts.
- Asset allowlist supports native ETH and ERC20 assets.
- Per-deposit cap is enforced for each configured asset.
- Total pilot cap is cumulative and does not reopen after release.
- Deposit pause blocks deposits.
- Emergency stop blocks deposits and releases.
- Owner controls token configuration, pause, emergency stop, and release authority updates.
- Release authority is separated from owner for release calls.
- Deposit accounting uses nonces and deterministic `depositId`.
- Release accounting uses `releaseId` replay protection.

Relayer-facing event:

```solidity
event BridgeDeposit(
bytes32 indexed depositId,
uint256 indexed sourceChainId,
address indexed sender,
address lockbox,
address token,
uint256 amount,
bytes32 flowchainRecipient,
uint256 nonce,
bytes32 metadataHash,
bytes32 pilotModeTag
);
```

Test proof:

- `forge test --match-path tests/bridge/BaseBridgeLockbox.t.sol` passed with 16 tests.
- `forge test` passed with 85 tests.

Covered refusal branches:

- zero amount
- unsupported token
- exceeded per-deposit cap
- exceeded total pilot cap
- paused deposits
- emergency stop
- wrong release authority
- duplicate release
- mismatched release token
- unavailable release amount
- zero evidence hash
44 changes: 44 additions & 0 deletions docs/agent-runs/production-l1-bridge/CREDIT_APPLICATION_PROOF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Credit Application Proof

Status: implemented and tested in local pilot state.

Credit path:

1. Observe Base deposit evidence.
2. Derive deterministic observation ID.
3. Derive deterministic replay key.
4. Derive deterministic credit ID.
5. Verify source chain is Base `8453`.
6. Verify source lockbox is approved.
7. Verify token is configured.
8. Verify amount is within the configured caps.
9. Apply local FlowChain credit state exactly once.
10. Write runtime handoff and application state.

Exactly-once state:

- State file: `services/bridge-relayer/out/real-value-pilot-e2e/bridge-credit-application-state.json`
- Replay key: `0xea93b7d168d2f1f6c4be4f95ba4d85aa2d07fc4298a720d180000a19d98481f0`
- First application: `applied`
- Same-event replay: `idempotent_replay`
- Duplicate fixture replay: rejected with `duplicate_replay_key`

Mock E2E deterministic IDs:

- observation ID: `0x01d76831a495a9869e1f880ae44fdf6b382bc1a2c0fe593e5536a9538989b73b`
- credit ID: `0x6f9e131efd014f742a589e62393bce237d9daee3ef7cd4ef9c0b7f5e95d10dc6`

Local usage artifact:

- `services/bridge-relayer/out/real-value-pilot-e2e/bridge-local-usage-proof.json`
- transfer ID: `0x7b0654d6bf64c91e835eebafd17cc1c6e55aa7b555d02f533a59170cab46be5b`
- credited wallet after credit: `20000000`
- credited wallet after prepared transfer: `10000000`
- second wallet after prepared transfer: `10000000`

Commands:

```powershell
npm run bridge:local-credit:smoke
npm run bridge:pilot:mock:e2e
```
Loading