diff --git a/.vitepress/config.ts b/.vitepress/config.ts index b49dda4..9ffffa9 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -20,16 +20,23 @@ export default withMermaid({ themeConfig: { nav: [ + { text: 'Overview', link: '/overview' }, { text: 'Design document', link: '/design-document' }, { text: 'Sub-solver guide', link: '/guides/sub-solver-integration' }, { text: 'Glossary', link: '/glossary' }, ], sidebar: [ + { + text: 'Overview', + items: [{ text: 'What is BYOS', link: '/overview' }], + }, { text: 'Specification', items: [ { text: 'Design document', link: '/design-document' }, + { text: 'Contracts reference', link: '/contracts' }, + { text: 'Service architecture', link: '/service' }, { text: 'Glossary', link: '/glossary' }, ], }, diff --git a/README.md b/README.md index 5e454c9..c8efe7f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,9 @@ The local path is what agents, offline readers, and `grep` use, and it is pinned ## Where to start -**Auditors** — [the design document](design-document.md), top to bottom. It is normative: where an implementation disagrees with it, the implementation is wrong, unless the document carries a dated revision note saying otherwise. The status table at the top says what is built today. The adversarial isolation proof is in [trampoline / settlement isolation](security/trampoline-settlement-isolation.md). +**New to BYOS?** — [What is BYOS](overview.md). The problem it solves, how it works at a high level, what sub-solvers get and don't get, why proposals get discarded, and the slashing policy. + +**Auditors** — [the design document](design-document.md), top to bottom. It is normative: where an implementation disagrees with it, the implementation is wrong, unless the document carries a dated revision note saying otherwise. The status table at the top says what is built today. The adversarial isolation proof is in [trampoline / settlement isolation](security/trampoline-settlement-isolation.md). The [contracts reference](contracts.md) and [service architecture](service.md) pages consolidate interfaces, interactions, and design decisions from the implementation repos. **Sub-solvers** — [the integration guide](guides/sub-solver-integration.md). It is the path from zero to a settled proposal, and it links into the design document for anything normative. diff --git a/contracts.md b/contracts.md new file mode 100644 index 0000000..42d95fb --- /dev/null +++ b/contracts.md @@ -0,0 +1,275 @@ +# Contracts reference + +The on-chain component of BYOS: three contracts that hold collateral, sandbox route execution, and anchor EIP-712 proposal signatures. All are immutable — no proxies, no upgrade keys. A v2 means a new deployment. + +The authoritative Solidity interfaces, NatSpec, and tests live in [`bleu/byos-contracts`](https://github.com/bleu/byos-contracts). This page is a reference summary; the [design document](design-document) is normative on semantics. + +## Contract topology + +```mermaid +flowchart TB + subgraph On-chain + E[Escrow
ERC20 + AccessControl] + F[TrampolineFactory
CREATE2 deployer + EIP-712 domain] + T1[Trampoline A] + T2[Trampoline B] + T3[Trampoline ...] + S[GPv2Settlement] + end + + E -->|constructor deploys| F + E -->|deposit triggers ensureDeployed| F + F -->|CREATE2| T1 & T2 & T3 + S -->|calls execute| T1 & T2 & T3 + T1 & T2 & T3 -->|reads SUBMITTER_ROLE| E + T1 & T2 & T3 -->|sweeps tokens back| S +``` + +Each sub-solver gets exactly one Trampoline instance, deployed at a deterministic CREATE2 address derived from their address. The Escrow, factory, and EIP-712 domain form one deployment generation — a factory redeployment invalidates all outstanding proposal signatures. + +## Escrow + +A per-chain, native-token **ERC20 contract** (inheriting OpenZeppelin's ERC20 + AccessControlDefaultAdminRules) holding sub-solver collateral. Tokens are minted 1:1 with deposited ETH and burned on withdrawal or debit. + +**Core invariant:** `totalSupply() + accumulatedDebits == address(this).balance` + +### Roles + +| Role | Holder | Powers | Limits | +|---|---|---|---| +| **Owner** (DEFAULT_ADMIN_ROLE) | Multisig / Safe | Set cooldown, grant/revoke roles, transfer ownership (two-step), receive debited funds | — | +| **Operator** (OPERATOR_ROLE) | BYOS service EOA | `debit`, `freeze`, `unfreeze`, `pause`, `unpause` | Cannot withdraw funds, change config, or grant submitters | +| **Submitter** (SUBMITTER_ROLE) | Solver EOA + auxiliary accounts | Identified by `tx.origin` in `Trampoline.execute` | No escrow authority at all; exists only for the Trampoline gate | + +A compromised operator can grief (debit falsely, freeze, pause) but cannot steal — debited funds always go to the Owner. The Owner can replace the operator immediately. + +### Functions + +#### Anyone + +| Function | What it does | +|---|---| +| `deposit(address subSolver) payable` | Mints tokens 1:1 with ETH. Deploys the Trampoline if this is the first deposit for that address. Reverts if `msg.value == 0` or the receiver has a pending withdrawal. | +| `withdrawDebits()` | Sweeps `accumulatedDebits` to `defaultAdmin()`. Callable by anyone (keeper-friendly). Reverts if admin renounced or nothing to sweep. | + +#### Sub-solver + +| Function | What it does | +|---|---| +| `requestWithdrawal()` | Signals intent to withdraw the entire balance. `effectiveBalance` drops to zero immediately (sub-solver is offline for proposals). Starts the cooldown clock. | +| `executeWithdrawal()` | After cooldown: burns tokens, sends ETH. Reverts if frozen, paused, cooldown not elapsed, or no balance. | +| `cancelWithdrawal()` | Aborts the request, restores `effectiveBalance`. Callable regardless of freeze/pause state. | + +#### Operator + +| Function | What it does | +|---|---| +| `debit(address subSolver, uint256 amount, bytes32 reason)` | Burns tokens, accumulates to `accumulatedDebits`. Works on frozen addresses and during pause. Reverts if `amount > balanceOf`. | +| `freeze(address subSolver)` | Blocks `executeWithdrawal` and transfers for this address. No-op if already frozen. | +| `unfreeze(address subSolver)` | Restores withdrawal and transfer ability. No-op if not frozen. | +| `pause()` | Global emergency brake: blocks all transfers and `executeWithdrawal`. Deposits, debits, and withdrawal requests stay open. | +| `unpause()` | Restores normal operation. | + +#### Owner + +| Function | What it does | +|---|---| +| `setCooldownPeriod(uint256 period)` | Updates the withdrawal cooldown. | +| `grantRole / revokeRole` | Manage OPERATOR_ROLE and SUBMITTER_ROLE. | +| `beginDefaultAdminTransfer / acceptDefaultAdminTransfer` | Two-step ownership transfer (prevents address typos from bricking the contract). | + +#### Views + +| Function | Returns | +|---|---| +| `balanceOf(address)` | Token balance (single source of truth for escrow). | +| `effectiveBalance(address)` | 0 if withdrawal pending, else `balanceOf`. Freeze does not affect it. | +| `withdrawableBalance()` | Accumulated debit pool available to the Owner. | +| `frozen(address)` | Whether an address is frozen. | +| `cooldownPeriod()` | Current cooldown in seconds. | +| `withdrawalRequestedAt(address)` | Request timestamp, or 0. | +| `paused()` | Global pause state. | + +### Events + +| Event | When | +|---|---| +| `Deposited(address subSolver, uint256 amount)` | Sub-solver's balance increased. | +| `Debited(address subSolver, uint256 amount, bytes32 reason)` | Operator penalized a sub-solver. `reason` = tx hash (Track A) or claim id (Track B). | +| `Withdrawn(address subSolver, uint256 amount)` | Sub-solver withdrew after cooldown. | +| `Frozen(address subSolver)` | Address frozen (Track B investigation). | +| `Unfrozen(address subSolver)` | Address unfrozen. | +| `WithdrawalRequested(address subSolver)` | Withdrawal intent registered. | +| `WithdrawalCancelled(address subSolver)` | Withdrawal aborted. | +| `DebitsWithdrawn(address to, uint256 amount)` | Accumulated debits swept to Owner. | +| `CooldownPeriodUpdated(uint256 oldPeriod, uint256 newPeriod)` | Cooldown changed. | +| `Paused(address account)` / `Unpaused(address account)` | Global pause toggled. | +| `Transfer` / `Approval` | Standard ERC20 (inherited). | + +### Transfer restrictions + +The token is deliberately transfer-restricted — it represents escrowed collateral, not a tradeable asset. + +| Condition | Transfer | Mint (deposit) | Burn (debit/withdrawal) | +|---|---|---|---| +| Paused | blocked | allowed | no restriction | +| Sender frozen | blocked | n/a | no restriction | +| Receiver frozen | blocked | allowed | n/a | +| Sender withdrawing | blocked | n/a | no restriction | +| Receiver withdrawing | blocked | blocked | n/a | + +Transfers exist for **key rotation**: `transfer(newAddress, fullBalance)` moves collateral without the uncollateralized gap of a withdraw-and-redeposit cycle. Both `transfer` and `transferFrom` call `ensureDeployed` on the recipient. + +### How BYOS interacts with Escrow + +- **At validation** (every tick): reads `effectiveBalance(subSolver)` to gate proposal eligibility. Cached with a short TTL. +- **At Track A penalty**: calls `debit(subSolver, amount, txHash)` after a reverted settlement. Reads `eth_getTransactionReceipt` first to determine `gas used × gas price`. +- **At Track B investigation**: calls `freeze(subSolver)` on receipt of a CoW EBBO certificate. Calls `debit` if upheld, `unfreeze` if overturned. +- **Incident response**: `pause()` → trace `Transfer` events → `freeze` tainted addresses → `unpause()` → `debit` at leisure. + +## Trampoline + +A per-sub-solver execution sandbox. Receives the sell token, runs the sub-solver's signed route, sweeps both trade tokens back to `GPv2Settlement`, and enforces the `buyAmount` floor via a balance-delta check. Holds **zero balance at rest** — a planted approval over an empty contract drains nothing. + +### Functions + +#### Primary execution + +```solidity +function execute( + Proposal calldata _proposal, + Interaction[] calldata _interactions, + address _sellToken, + address _buyToken, + bytes calldata _signature +) external +``` + +Only callable when all gates pass: +1. `msg.sender == GPv2Settlement` — must be within a settlement context. +2. `tx.origin` holds `SUBMITTER_ROLE` on the Escrow — must be BYOS's submitter, not a rival solver replaying public calldata. +3. `block.timestamp <= proposal.validUntil` — proposal not expired. +4. `proposal.nonce` not previously used — replay protection. +5. EIP-712 signature over `ProposalData + interactionsHash` recovers to `SUB_SOLVER` — proves the sub-solver consented to this exact route. + +Then: +1. Records `GPv2Settlement`'s current buy-token balance. +2. Executes each interaction as `call(gas, target, value, calldata)`. +3. Sweeps full remaining balance of both trade tokens back to `GPv2Settlement`. +4. Asserts the settlement's buy-token balance grew by at least `buyAmount`. Reverts if not. + +Emits `Executed(orderUidHash, delta, floor)`. + +#### Residue claim + +| Function | What it does | +|---|---| +| `claimToken(address token, address recipient)` | Sub-solver only. Transfers full balance of `token` to `recipient`. Use `BUY_ETH_ADDRESS` for native ETH. | +| `claimTokens(address[] tokens, address recipient)` | Batch claim. | + +These exist for intermediate-token dust and stray transfers. Trade tokens are swept by `execute` and never strand. + +#### Views + +| Function | Returns | +|---|---| +| `SUB_SOLVER()` | The sub-solver address (proposal signatures must recover to this). | +| `SETTLEMENT()` | `GPv2Settlement` address (only allowed `execute` caller). | +| `DOMAIN_SEPARATOR()` | EIP-712 domain from the deploying factory. | +| `ESCROW()` | Escrow address (submitter registry). | +| `noncesUsed(uint256)` | Whether a nonce has been consumed. | + +### Events + +| Event | When | +|---|---| +| `Executed(bytes32 orderUidHash, uint256 delta, uint256 floor)` | Route executed. `delta` = actual buy-token balance growth; `floor` = signed `buyAmount`. | +| `ResidueClaimed(address token, uint256 amount, address recipient)` | Sub-solver claimed residue. | + +### Errors + +| Error | Cause | +|---|---| +| `Trampoline_OnlySettlement()` | Caller is not `GPv2Settlement`. | +| `Trampoline_UnauthorizedSubmitter()` | `tx.origin` does not hold `SUBMITTER_ROLE`. | +| `Trampoline_ProposalExpired()` | `block.timestamp > validUntil`. | +| `Trampoline_NonceAlreadyUsed()` | Nonce was consumed in a prior execution. | +| `Trampoline_InvalidSignature()` | Recovered signer is not `SUB_SOLVER`. | +| `Trampoline_FloorNotMet(uint256 delta, uint256 floor)` | Route delivered less than the signed floor. Settlement reverts entirely. | +| `Trampoline_OnlySubSolver()` | `claimToken` / `claimTokens` caller is not the sub-solver. | +| `Trampoline_EthClaimFailed()` | Native ETH transfer failed during `claimToken`. | + +### How BYOS interacts with Trampoline + +- **At simulation**: builds a full `settle()` call via `eth_estimateGas` that includes `trampoline.execute(...)`. Uses state overrides for `AnyoneAuthenticator` and `SUBMITTER_ROLE`. +- **At settlement**: the driver's encoded calldata includes two interactions — `sellToken.transfer(trampoline, sellAmount)` followed by `trampoline.execute(proposal, route, ...)`. The Trampoline address is computed from the sub-solver address via CREATE2, never stored. +- **Never directly writes to Trampoline state.** All state changes happen within the `execute` call during settlement. + +## TrampolineFactory + +CREATE2 deployer for Trampoline instances. Also anchors the EIP-712 domain separator that binds all proposal signatures to this deployment generation. + +### Functions + +| Function | What it does | +|---|---| +| `ensureDeployed(address subSolver) → address` | Idempotent CREATE2 deployment. Returns the instance address. Callable by anyone. Salt = `bytes32(uint256(uint160(subSolver)))`. | +| `addressOf(address subSolver) → address` | Computes the deterministic CREATE2 address. Works before deployment (counterfactual). | +| `domainSeparator() → bytes32` | The EIP-712 domain separator. | +| `SETTLEMENT() → address` | `GPv2Settlement` address baked into instances. | +| `ESCROW() → address` | Escrow address baked into instances. | + +### Events + +| Event | When | +|---|---| +| `TrampolineDeployed(address subSolver, address instance)` | First deployment for a sub-solver. | + +### How BYOS interacts with TrampolineFactory + +- **At validation**: calls `addressOf(subSolver)` to resolve the Trampoline address for simulation. Cached — addresses are immutable. +- **At settlement crafting**: uses `addressOf` to compute the CREATE2 address for encoding the `transfer` and `execute` interactions. Pure local computation (keccak256 + ABI encoding), no RPC. + +## Key design decisions + +Rationale for each decision lives in the [ADRs in `byos-contracts`](https://github.com/bleu/byos-contracts/tree/main/docs/adr). This section summarizes the final state. + +### One Trampoline instance per sub-solver + +Each sub-solver gets its own isolated sandbox at a deterministic CREATE2 address. This confines approvals and residue to the originating sub-solver, enables on-chain attribution (the CREATE2 address in calldata identifies who ran), and permits safe approval reuse across that sub-solver's settlements. + +### ERC20 for escrow balance + +Using an ERC20 (rather than a plain `mapping`) enables collateral transfer for key rotation without the uncollateralized gap of a withdraw-and-redeposit cycle. The token is transfer-restricted and deliberately won't integrate with DeFi. + +### Blanket operator debit authority + +The operator can debit any sub-solver up to their full balance without per-proposal signature gating. Per-debit EIP-712 verification was rejected: it adds gas and complexity for marginal benefit, since the operator is already trusted and debited funds go to the Owner, not the operator. + +### Signature-gated execution (non-repudiation) + +The sub-solver's EIP-712 signature over the proposal (including `interactionsHash`) means BYOS cannot fabricate faults by substituting different interactions. A reverted settlement's calldata proves exactly what the sub-solver authorized, making Track A debits verifiable by any third party. + +### Submitter gate via `tx.origin` + +Once BYOS settles a proposal, its signature and route are public calldata. The `tx.origin` must hold `SUBMITTER_ROLE` on the Escrow, preventing rival solvers from replaying the `execute()` call in their own settlements. Covers both direct submission and CoW's `Solver7702Delegate` auxiliary accounts. + +### Balance-delta floor check + +`execute` measures the settlement's buy-token balance growth rather than checking an exact transfer amount. This supports over-delivery, both order kinds, and routes that deliver output directly to the settlement rather than to the instance. + +### Nonce-based replay protection + +Each Trampoline instance tracks used nonces in a `mapping(uint256 => bool)`. Nonces are unordered — any `uint256` is valid as long as it hasn't been consumed. This provides hard replay protection independent of BYOS trust, at the cost of 20k gas for the first use of each nonce. + +### Deposit-time Trampoline deployment + +Deploying the instance when escrow is deposited (not lazily during settlement) keeps the settlement hot path clean — no per-settlement deployment gas, no existence guard. The one-time deploy cost is paid by the depositor. + +### Immutable contracts, no proxies + +No proxy, no upgrade key. A v2 is a new deployment. The factory redeployment invalidates all outstanding signatures. + +### Single-order solutions + +One proposal commits to one order, and one settlement carries one proposal. Under the fair combinatorial auction (CIP-67), coincidence of wants is small and netting surplus rarely exists. Sub-solvers are DEXes and routing APIs that want to quote and sign one order at a time. diff --git a/design-document.md b/design-document.md index 1e7493c..795326e 100644 --- a/design-document.md +++ b/design-document.md @@ -64,7 +64,7 @@ BYOS requires **no changes to the CoW auction or competition**. It is a black bo The design problem is that CoW's safety model does not fit. `settle` is `onlySolver`, gated by a manager-curated allowlist; vouched solvers post a bond; a circuit breaker slashes or jails misbehavior. CoW trusts a permissioned, bonded set and punishes them rather than constraining what interactions may do. Sub-solvers are permissionless and unbonded — exactly the actor that model refuses to let near `settle`. -So BYOS rebuilds the boundary structurally rather than socially. The Trampoline replaces the `onlySolver` allowlist with a sandbox. Escrow replaces the DAO bond. Debit and slash replace circuit-breaker slashing. Each substitution is load-bearing, and the rest of this document is what they mean concretely. +So BYOS rebuilds the boundary structurally rather than socially. The Trampoline replaces the `onlySolver` allowlist with a sandbox. Escrow replaces the DAO bond. Debit and slash replace circuit-breaker slashing. ## Order flow @@ -217,7 +217,7 @@ Per-instance isolation earns its keep on three things a shared trampoline cannot - `tx.origin` holds the Escrow's `SUBMITTER_ROLE` — a settlement submitted by BYOS. - The sub-solver's EIP-712 signature over the route verifies, and `validUntil` has not passed. -**Signature-gating** exists so a reverted settlement self-evidences exactly what the sub-solver authorized: the signed data is in the calldata, recoverable from the transaction. This makes Track A debits verifiable by any third party rather than only by BYOS. Without it, BYOS could substitute different interactions, submit a settlement that reverts, and debit the sub-solver for a fault it manufactured. The cost is a single `ecrecover` per settlement, negligible against DEX swap costs. +**Signature-gating** exists so a reverted settlement self-evidences exactly what the sub-solver authorized: the signed data is in the calldata, recoverable from the transaction. This makes Track A debits verifiable by any third party rather than only by BYOS. Without it, BYOS could substitute different interactions, submit a settlement that reverts, and debit the sub-solver for a fault it manufactured. **The submitter gate** exists because once BYOS settles a proposal, its signature and route are public calldata. While `validUntil` is live, any other allow-listed CoW solver could replay or front-run the `execute` in its own settlement, rerunning the signed route outside BYOS's control and muddying attribution. `SUBMITTER_ROLE` is granted by the Owner on the Escrow, which therefore acts as the submitter registry for its contract generation. It covers both the allow-listed solver EOA and, for CoW's `Solver7702Delegate` parallel path, each approved auxiliary account — there the auxiliary account, not the solver EOA, is `tx.origin`. @@ -235,7 +235,7 @@ Anyone may deposit for a sub-solver. The sub-solver withdraws subject to a coold The contract is a **dumb ledger**. It enforces bounds — who may debit, cooldown, pause, freeze, transfer restrictions — but never the correctness of a debit's reason. Reserve calculations, proposal eligibility, and transfer-chain debit caps live in the service. -**Deployment is immutable.** No proxy, no upgrade key. Immutability is a trust signal for sub-solvers: the code they deposit into will not change. A v2 means a new deployment, and the cooldown-based withdrawal makes migration straightforward. The Escrow's constructor deploys the Trampoline factory itself, taking the `GPv2Settlement` address rather than a factory address: instances bind to the Escrow as their submitter registry, and the factory needs the Escrow address before the Escrow could otherwise exist. Escrow, factory, and EIP-712 domain therefore form one deployment generation. +**Deployment is immutable.** No proxy, no upgrade key. A v2 means a new deployment. The Escrow's constructor deploys the Trampoline factory itself, taking the `GPv2Settlement` address rather than a factory address: instances bind to the Escrow as their submitter registry, and the factory needs the Escrow address before the Escrow could otherwise exist. Escrow, factory, and EIP-712 domain therefore form one deployment generation. ### Escrow roles @@ -348,7 +348,7 @@ Eip712Domain { **The nonce is a unique salt with no enforcement**, on-chain or off-chain. It makes each proposal's EIP-712 hash distinct; there is no ordering or uniqueness rule. Fill tracking alone would not prevent replay of `execute`, since a settlement need not include the order at all, so a third party could rerun a live proposal in a tradeless settlement. Third-party replay is blocked by the submitter gate instead ([`#execution-authority`](#execution-authority)). Replay by BYOS's own submitter remains possible by design: BYOS is trusted not to resubmit, `validUntil` bounds the window and is enforced on-chain, and a filled order cannot be settled again. Keeping the Trampoline storage-free is worth more than an on-chain nonce mapping. -**The payload is raw interactions**, `Vec<{target, value, calldata}>` — arbitrary calls against any DEX or protocol, executed as-is. Structured routes would let BYOS author every call and forbid sub-solver approvals outright, but they would kill any-DEX generality, require BYOS to maintain a venue registry, and bottleneck sub-solver innovation. Containment is the Trampoline's job, structurally. The sub-solver is fully responsible for the complete route, including required hooks and approvals; BYOS can accept or reject at gatekeeping, never patch. +**The payload is raw interactions**, `Vec<{target, value, calldata}>` — arbitrary calls against any DEX or protocol, executed as-is. Structured routes would let BYOS author every call and forbid sub-solver approvals outright, but they would kill any-DEX generality and require BYOS to maintain a venue registry. Containment is the Trampoline's job, structurally. The sub-solver is fully responsible for the complete route, including required hooks and approvals; BYOS can accept or reject at gatekeeping, never patch. The **factory is a domain anchor**. Binding `verifyingContract` to the TrampolineFactory cleanly separates contract generations: v1 signatures do not verify against a v2 factory. A factory redeployment invalidates all outstanding signatures, so sub-solver clients must update their domain configuration. @@ -535,7 +535,7 @@ Before simulating, the order and proposal pair must pass a cheap envelope check All four signature schemes are supported, since the scheme is encoded in the trade flags and GPv2 verifies it for real during simulation. Sell and buy orders are both supported, including native-ETH buys. Order hooks are included in the simulation for accurate gas, using the order's pre-encoded interactions from the orderbook; the `/solve` response does not include hooks, because the driver appends the order's own hooks itself. -**A revert is terminal on the first occurrence.** No strikes, no retry. A proposal that reverted once is not robust enough to offer to `/solve` — if it won and then reverted on-chain, the sub-solver takes a Track A penalty, which is strictly worse for them than resubmitting. Resubmission is the sub-solver's "I still believe in this route" signal. Transport errors are different: an RPC timeout or DNS failure defers to the next tick rather than punishing the sub-solver, and orderbook 404s reject while transient orderbook errors defer. +**A revert is terminal on the first occurrence.** No strikes, no retry. A proposal that reverted once is not offered to `/solve` — if it won and then reverted on-chain, the sub-solver takes a Track A penalty. Transport errors are different: an RPC timeout or DNS failure defers to the next tick rather than punishing the sub-solver, and orderbook 404s reject while transient orderbook errors defer. **The profitability gate runs on the first simulation only.** A score of zero or less rejects as unprofitable, matching `/solve`'s own inclusion rule, so one invariant holds: an `Active` proposal is one that could win an auction right now. It is not re-applied on re-validation, because gas prices wobble and rejecting on a spike would churn proposals that are profitable again two blocks later. @@ -564,7 +564,7 @@ Settlement overhead is therefore paid per order and never amortized, and netting - **Surplus** is the improvement beyond the order's limit price — extra buy tokens on a sell order, sell tokens kept back on a buy order — converted at the auction's reference price. - **Gas** is the simulated `eth_estimateGas` result plus a 30k buffer, cached on the proposal, times the auction's effective gas price. The buffer is small because the full-settle estimate already covers intrinsic gas and the whole settlement path, so it only absorbs warm and cold storage differences and driver batching variance. -**There is no fee term, and its absence is deliberate.** CoW's score is surplus plus protocol fees and nothing else; gas never appears as a subtraction there. It reaches the score only because a solver declares gas as its own fee, which lowers what the user receives, which lowers surplus. The protocol fee then cancels out of any ranking — it is carved out of surplus and added straight back — so `score = route surplus − our own cut`. Once the cut equals the gas cost ([`#gas`](#gas)), `surplus − gas` is the score the autopilot will compute for the bid. +**There is no fee term.** CoW's score is surplus plus protocol fees and nothing else; gas never appears as a subtraction there. It reaches the score only because a solver declares gas as its own fee, which lowers what the user receives, which lowers surplus. The protocol fee then cancels out of any ranking — it is carved out of surplus and added straight back — so `score = route surplus − our own cut`. Once the cut equals the gas cost ([`#gas`](#gas)), `surplus − gas` is the score the autopilot will compute for the bid. **BYOS does not estimate protocol fees either.** The driver applies them itself, then encodes and simulates before bidding; a solution that cannot absorb the fee fails that simulation and is dropped, which costs the round but produces no revert, no penalty, and no escrow debit. It is also impossible to estimate before `/solve`, since fee policies are built per auction by the autopilot and delivered only in the `/solve` payload. @@ -675,9 +675,9 @@ Track A and Track B penalties for the same settlement **stack**. There is no cre `c_l` is read from CoW's reward mechanism at debit time, with a hardcoded fallback for v1. Current values: **0.010 ETH** on Ethereum, **10 xDAI** on Gnosis. -**Minimum escrow balance** is sized to cover worst-case Track A for a single settlement, `gas + c_l`. That keeps the barrier to entry low, which matters for a permissionless system. Track B is inherently under-collateralized regardless of the minimum, so a higher one would buy little. +**Minimum escrow balance** is sized to cover worst-case Track A for a single settlement, `gas + c_l`. -**On shortfall**, BYOS drains the remaining balance and absorbs the difference. The sub-solver is naturally suspended, since zero collateral means ineligible. There is no permanent ban and no debt tracking — bans are meaningless when a new address is a new identity, and the escrow loss is the penalty. +**On shortfall**, BYOS drains the remaining balance and absorbs the difference. The sub-solver is suspended (zero collateral means ineligible). There is no permanent ban and no debt tracking. The **policy is immutable for v1**. No unilateral updates; a change requires a v2 policy with a new escrow deployment or a migration. @@ -692,7 +692,7 @@ Routine, fast, provable. | Dispute | sub-solver | 72h window on narrow grounds: wrong attribution, the transaction did not revert, the amount exceeds `gas + c_l` | 72h | | Resolution | BYOS | reviews and decides, unilaterally | after the window | -Track A is BYOS-unilateral because for reverts and deadline misses everything is on-chain verifiable: the receipt, the gas cost, and the Trampoline CREATE2 address that identifies the sub-solver. A provably incorrect debit is an operational bug, not a policy failure. +Track A is BYOS-unilateral because for reverts and deadline misses everything is on-chain verifiable: the receipt, the gas cost, and the Trampoline CREATE2 address that identifies the sub-solver. **Non-settlement is detected from driver notifications**: a `Cancelled`, `Expired`, or `Fail` for an `Executing` proposal means the driver confirmed it began submitting and then abandoned the settlement with no transaction landing. That covers both submission failures and the driver's own block deadline. An executing *timeout* is deliberately not charged — a lost notification is not proof of non-settlement. This sub-category rests on BYOS's internal auction records and is not independently verifiable by the sub-solver, which is an accepted trust assumption. @@ -722,23 +722,23 @@ Track B stays out of the proposal state machine: a ruling months later is an acc The 36h sub-solver window is tight, and permissionless participants without responsive operations may struggle. It is what remains after BYOS reserves the other 36h of its own 72h CoW window to process and relay. -**Track B has an unrecoverable gap.** If the sub-solver has withdrawn, or the escrow is smaller than the claim, BYOS absorbs the difference. This is why gatekeeping is mandatory: it is the primary Track B defense, and escrow cannot be. +**Track B has an unrecoverable gap.** If the sub-solver has withdrawn, or the escrow is smaller than the claim, BYOS absorbs the difference. ### Attribution **One sub-solver per settlement transaction.** The per-sub-solver Trampoline CREATE2 address in the settlement calldata self-evidences which sub-solver's route ran, with no reliance on BYOS's private records. That is what makes Track A debits indisputable and Track B attribution clean. -The cost is less batching efficiency, accepted because clean attribution is worth more than marginal gas savings. +The cost is less batching efficiency. Off-chain, notifications carry auction and solution ids rather than proposals, so attribution to a proposal is a join through the `solutions` mapping the engine writes before bidding ([`#proposal-lifecycle`](#proposal-lifecycle)). The Trampoline address in calldata remains the on-chain proof, checked when debiting. ### Gatekeeping -Preventive, best-effort, and **non-exculpatory**. Before settling, BYOS validates that the proposal simulates without reverting, that required pre- and post-hooks from the order's app data are present in the interactions, and that the route is not obviously worse than reference AMM prices. +Preventive, best-effort, and **non-exculpatory**. Before settling, BYOS validates that the proposal simulates without reverting and that the route is not obviously worse than reference AMM prices. BYOS includes the order's pre- and post-hooks in the simulation for accurate gas estimation; the driver appends them to the settlement separately ([`#solver-engine`](#solver-engine)). -Sub-solvers are responsible for including required hooks in their own interactions. Some hooks change the token balances a route depends on — withdrawing DEX liquidity before a swap, for instance — so the sub-solver must see and simulate them to compute a correct route. BYOS rejects proposals missing required hooks before settlement, but passing gatekeeping does not absolve anyone: the EIP-712 signature is the sub-solver accepting responsibility for its complete route. +Sub-solvers do not include hooks in their signed interactions — those contain only the routing calls. However, some hooks change the token balances a route depends on — withdrawing DEX liquidity before a swap, for instance — so the sub-solver must account for hook effects when computing a correct route. Passing gatekeeping does not absolve anyone: the EIP-712 signature is the sub-solver accepting responsibility for the route it signed. -Simulation failures cost the sub-solver **nothing** beyond a rate-limit slot. Only on-chain failures debit escrow. Debiting simulation failures was rejected because they are usually environmental — a pool moved, the order filled elsewhere — so slashing them would punish honest participants and deter permissionless participation. +Simulation failures cost the sub-solver **nothing** beyond a rate-limit slot. Only on-chain failures debit escrow. Simulation failures are not debited. ### Transparency @@ -754,8 +754,6 @@ The `reason` field on `debit` carries the settlement transaction hash for a Trac **Strays are written off.** Tokens landing on an instance outside the settlement flow — mistaken transfers, airdrops, intermediate-token dust — are nobody's problem by design. A sub-solver with a standing route-planted approval can take them; preventing that is the un-enumerable approval-fighting problem the topology decision already rejected, and the amounts are donations and dust. Never user funds, trade capital, buffers, or escrow, all of which are protected by settlement atomicity and the floor check. If a sub-solver skims strays, the response is off-chain — gatekeeping, eviction — not a contract mechanism. -**In-route capture is tolerated.** A sub-solver can keep surplus by capturing it in-route before the sweep. That is bid-neutral: it touches only value above its own signed floor, which it could have kept by signing a higher floor. Guarding against it would reopen the filtered-approval arms race. Uncaptured padding is a donation to BYOS. +**In-route capture is tolerated.** A sub-solver can keep surplus by capturing it in-route before the sweep. That is bid-neutral: it touches only value above its own signed floor, which it could have kept by signing a higher floor. Guarding against it would reopen the filtered-approval arms race. The floor is the bid. A sub-solver signs the minimum it is sure to deliver, below its simulated route output, and margin sizing is its own tradeoff — too thin reverts and lands Track A debits, too thick loses auctions. -The floor is the bid. A sub-solver signs the minimum it is sure to deliver, below its simulated route output, and margin sizing is its own tradeoff — too thin reverts and lands Track A debits, too thick loses auctions. - -Because the instance is genuinely empty at rest, "the instance is not a wallet" is literal: a planted approval over an empty contract drains nothing. +The instance is empty at rest; a planted approval over an empty contract drains nothing. diff --git a/guides/sub-solver-integration.md b/guides/sub-solver-integration.md index a286191..822a1b3 100644 --- a/guides/sub-solver-integration.md +++ b/guides/sub-solver-integration.md @@ -1,118 +1,178 @@ # Sub-solver integration -The path from zero to a settled proposal. +This guide tells you how to go from zero to a settled proposal. -This guide is about sequence and gotchas. Every normative fact — field names, amounts, signature shapes, penalty numbers — lives in [the design document](../design-document) or the [OpenAPI document](https://github.com/bleu/byos-service/blob/main/crates/byos/openapi.yml), and is linked rather than repeated. If this guide and one of those ever disagree, they are right. +All normative facts (field names, amounts, signature formats, penalty amounts) are in [the design document](../design-document) or the [OpenAPI document](https://github.com/bleu/byos-service/blob/main/crates/byos/openapi.yml). This guide links to them and does not repeat them. If this guide and one of those disagree, the source document is correct. -You do not need a CoW solver seat, an allowlist entry, or a relationship with the CoW DAO. You need an address, collateral in the escrow, and the ability to sign EIP-712 messages and quote a route. +You do not need a CoW solver seat, an allowlist entry, or a relationship with CoW DAO. You need an address, collateral in the Escrow, and the ability to sign EIP-712 messages. -## 1. What you are signing up for +## Your role as a sub-solver -You compute routes. BYOS bids them into CoW's auction under its own bonded solver seat, submits the settlement, and takes the consequences from the protocol. When a settlement carrying your route fails on-chain, BYOS charges that cost back to your escrow balance. **This is real money, debited without asking you first**, on the terms in [`#penalties`](../design-document#penalties). +**You are responsible for:** -The two things worth internalizing before you write any code: +- **Collateral.** Deposit funds into the Escrow. Your balance must be more than one worst-case Track A debit (`gas + c_l`). +- **Order selection.** Find orders in CoW's public orderbook. Compute any route that delivers buy tokens to the GPv2Settlement contract. Assume execution from Trampoline with sell tokens on it. +- **Floor margin.** Set the `buyAmount` floor in your proposal. If the floor is too close to the route output, your route can revert on-chain (Track A debit). If the floor is too far below, you lose auctions. +- **Venue-level fees.** If your route goes through a pool you operate, you keep those fees. To capture surplus above your floor, do it inside your route before the sweep. Any remaining tokens in the Trampoline belong to you to claim or use in future trades. +- **Responding to Track B claims** within the 36-hour challenge window. Claims can arrive months after a trade. -Your signature covers your **complete route**, including any pre- and post-hooks the order's app data requires. BYOS checks for them at gatekeeping and rejects proposals that omit them, but passing that check does not transfer liability. Gatekeeping is preventive and explicitly [non-exculpatory](../design-document#gatekeeping). +**You are NOT responsible for:** -The `buyAmount` you sign is a **floor, not a quote**. The contract enforces it as a minimum and reverts below it. Sizing that margin is your tradeoff and nobody else's: too thin and routes revert on-chain and cost you a [Track A](../design-document#track-a) debit, too thick and you lose auctions to sub-solvers who bid tighter. +- **Transaction submission.** BYOS builds and submits the settlement through the CoW driver. You never call `settle`. +- **Scoring.** BYOS scores proposals (`surplus - gas`), selects the best one per order, and bids it into CoW's auction. +- **Gas estimation or fee calculation.** BYOS sizes the gas cut. The driver applies protocol and partner fees. Your amounts are raw, pre-fee route amounts. +- **Trampoline contract logic.** The sweep, the floor check, and the sandbox isolation are in the contract code. You cannot change them. +- **CoW protocol compliance.** BYOS manages the relationship with CoW DAO, the bonding pool, and the reward accounting. But gatekeeping is non-exculpatory. Your signed route is your responsibility. + +## 1. Understand the risks + +You compute routes. BYOS bids them into CoW's auction under its own bonded solver seat. BYOS submits the settlement and takes the consequences from the protocol. + +When a settlement that carries your route fails on-chain, BYOS debits the cost from your escrow balance. **BYOS debits this amount without prior approval.** Read the terms in [`#penalties`](../design-document#penalties). + +**The `buyAmount` is a floor, not a quote.** The contract enforces it as a minimum. If the route delivers less than this amount, the settlement reverts. You set the margin between the floor and the expected route output. A [Track A](../design-document#track-a) debit is the penalty for a revert. A floor that is too far below the output loses auctions. ## 2. Deposit collateral -Deposit native token into the [Escrow](../design-document#escrow) for your address. Anyone may fund an address, but only that address can withdraw. +Deposit native token into the [Escrow](../design-document#escrow) for your address. Any address can fund a sub-solver address. Only the sub-solver address can withdraw. -Three things happen as a result: +The deposit causes three effects: -- You become eligible to submit proposals. The deposit *is* the permission — there is no allowlist. The minimum is sized to cover a single worst-case Track A debit ([`#penalties`](../design-document#penalties)). -- Your [Trampoline](../design-document#topology) instance is deployed, at a deterministic CREATE2 address derived from your address. You pay that one-time gas. Routes never execute anywhere else. -- Your rate limit is set. It scales with your balance ([`#proposal-api`](../design-document#proposal-api)), so a larger deposit buys throughput as well as eligibility. +1. **You can submit proposals.** The deposit is the only requirement. The minimum balance must be enough for a single worst-case Track A debit ([`#penalties`](../design-document#penalties)). +2. **BYOS deploys your [Trampoline](../design-document#topology) instance.** The instance has a deterministic CREATE2 address that is based on your address. You pay this one-time gas cost. All your routes execute in this instance. +3. **BYOS sets your rate limit.** The rate limit scales with your balance ([`#proposal-api`](../design-document#proposal-api)). A larger deposit gives more throughput. -Exiting is deliberately not instant. Withdrawal is all-or-nothing behind a cooldown, and requesting it takes you offline for new proposals immediately ([`#withdrawal-and-freeze`](../design-document#withdrawal-and-freeze)). Plan for that: a balance you might need to pull at short notice is not a balance you should be operating on. +### Withdrawal -To rotate keys, ERC20-`transfer` your escrow balance to the new address rather than doing a withdraw-and-redeposit cycle. Transfer avoids the uncollateralized gap. The new address gets its own Trampoline instance, and your old proposals do not follow you — your address is your identity in all three roles at once: proposal signer, escrow key, and CREATE2 salt ([`#proposal-schema`](../design-document#proposal-schema)). +Withdrawal is not instant. It is all-or-nothing with a cooldown period. When you request a withdrawal, your effective balance drops to zero immediately. You cannot submit proposals during the cooldown. See [`#withdrawal-and-freeze`](../design-document#withdrawal-and-freeze). -## 3. Find orders to route +### Key rotation + +To rotate keys, use the ERC20 `transfer` function to move your escrow balance to the new address. Do not withdraw and redeposit. A transfer prevents the gap where you have no collateral. -BYOS does not run an orderbook and does not push you work. Orders come from CoW's public orderbook API, the same source every other solver reads. +The new address gets its own Trampoline instance. Your old proposals do not follow you. Your address serves three roles: proposal signer, escrow key, and CREATE2 salt ([`#proposal-schema`](../design-document#proposal-schema)). -Not every order is routable through BYOS. The [validation envelope](../design-document#simulation) rejects partially fillable orders, bridging orders, and orders using external or internal balance flavors. Filter for those before spending compute on a quote. Sell and buy orders are both supported, including native-ETH buys, and all four CoW signature schemes work. +## 3. Find orders to route -One proposal covers exactly one order ([`#single-order-solutions`](../design-document#single-order-solutions)). There is no batch format and no netting across orders. +BYOS does not operate an orderbook. Orders come from CoW's public orderbook API. + +One proposal covers one order ([`#single-order-solutions`](../design-document#single-order-solutions)). There is no batch format. ## 4. Build a route -A route is a list of raw calls: target, value, calldata. Any DEX, any protocol, no venue registry, no approval from BYOS. They execute as-is inside your Trampoline instance. +A route is a list of raw calls. Each call has a target, a value, and calldata. You can use any DEX or protocol. BYOS does not maintain a venue registry. The calls execute as-is inside your Trampoline instance. -What the sandbox means for how you write one: +### Sandbox constraints -The instance holds only the sell amount BYOS pushes in for this settlement. It has no allowance over `GPv2Settlement` and no access to anyone else's instance. So a route that assumes it can reach protocol buffers, or that plants an approval expecting to use it against a funded contract later, gets nothing — the instance is empty between settlements by construction ([`#topology`](../design-document#topology)). +Your Trampoline instance holds only the sell amount that BYOS pushes in for this settlement. The instance has no allowance over `GPv2Settlement`. It has no access to other sub-solver instances. The instance is empty between settlements ([`#topology`](../design-document#topology)). -You do not need to return funds yourself. The Trampoline sweeps both trade tokens back to the settlement and enforces your floor, in contract code you cannot override ([`#order-flow`](../design-document#order-flow)). Delivering output straight to the settlement also works; the check measures what the settlement actually received. +A route that tries to access protocol buffers gets nothing. A route that plants an approval for future use against a funded contract gets nothing. -Anything you deliver above your floor is **not yours** once the sweep runs. It becomes BYOS-owned settlement slippage. If you want to keep surplus, capture it inside your route before the sweep, or sign a higher floor — both are fine and neither is penalized ([`#residue`](../design-document#residue)). +### Headroom -Leave headroom above the user's limit price. BYOS takes a [gas cut](../design-document#gas) sized at the estimated settlement cost, and CoW's driver applies protocol and partner fees on top of that, after your amounts. A route quoted exactly at the limit produces an infeasible solution and will be skipped. +Leave headroom above the user's limit price. BYOS takes a [gas cut](../design-document#gas) from the trade. The CoW driver applies protocol and partner fees on top. If a route quotes exactly at the limit, BYOS skips it because the solution is not feasible. -Amounts you sign are **raw, pre-fee route amounts**. Do not try to pre-subtract fees; the wedge is created downstream by the driver's price shift and lands in the settlement, not in your instance. +Sign **raw, pre-fee route amounts**. Do not pre-subtract fees. The driver creates the fee wedge after your amounts. The wedge stays in the settlement, not in your instance. ## 5. Sign the proposal -Sign the EIP-712 typed data in [`#proposal-schema`](../design-document#proposal-schema). The struct, the domain, and the typehash are owned by the contracts repo — derive them from [`bleu/byos-contracts`](https://github.com/bleu/byos-contracts) and test against the contract's own vectors rather than re-deriving them yourself. The same signature the API accepts is verified again on-chain by your Trampoline at settlement, so a mismatch fails late and expensively. +Sign the EIP-712 typed data described in [`#proposal-schema`](../design-document#proposal-schema). Get the struct, domain, and typehash from [`bleu/byos-contracts`](https://github.com/bleu/byos-contracts). Test your signatures against the contract's own test vectors. Do not derive the typehash yourself. + +The API verifies your signature at submission. The Trampoline verifies the same signature on-chain at settlement. If the two do not match, the settlement fails. + +### Domain binding + +The EIP-712 domain binds to the TrampolineFactory address. The domain is specific to a chain **and** a deployment generation. A contracts v2 deployment invalidates all outstanding signatures. Update your domain configuration when contracts change. + +### Route commitment + +The `interactionsHash` field in the signed struct commits to your route. BYOS cannot substitute different interactions. A third party can verify that the signed data matches the settlement calldata. This property makes Track A debits verifiable. -Two details that catch people: +### Expiry -The domain binds to the TrampolineFactory address, so it is per-chain **and per deployment generation**. A contracts v2 invalidates every outstanding signature and you must update your domain config. +Keep `validUntil` short. BYOS caps it at ingestion ([`#proposal-lifecycle`](../design-document#proposal-lifecycle)). A route that is more than a few minutes old is stale. -Your route is committed to by hash. BYOS cannot substitute different interactions behind your signature — that is deliberate, and it is what makes a Track A debit something a third party can verify rather than something you have to take BYOS's word for. +## 6. Submit and poll -Keep `validUntil` short. It is capped at ingestion ([`#proposal-lifecycle`](../design-document#proposal-lifecycle)), and a route priced longer ago than a few minutes is stale anyway. +Send a `POST` request with the proposal. See the [OpenAPI document](https://github.com/bleu/byos-service/blob/main/crates/byos/openapi.yml) for the payload format, status codes, and rejection reasons. -## 6. Submit, then poll +### API endpoints -`POST` the proposal. Endpoints, payload shape, status codes, and typed rejection reasons are in the [OpenAPI document](https://github.com/bleu/byos-service/blob/main/crates/byos/openapi.yml). +All endpoints are on the public listener (default port 9585): -**A `2xx` is not acceptance.** It means "accepted for validation" and hands you an id. Escrow checks and simulation run in a background loop, not on the request path ([`#proposal-api`](../design-document#proposal-api)). Integration code that treats a `2xx` as "my proposal is live" is wrong. +| Method | Path | Auth | Purpose | +|---|---|---|---| +| `POST` | `/proposals` | Proposal signature (in body) | Submit a signed proposal. Returns `202` with an id. This is **not** acceptance. | +| `GET` | `/proposal/{id}` | `X-Signature` (EIP-712 `ReadAuth`) | Get your proposal status, rejection reason, and settlement/penalty tx hashes. | +| `GET` | `/proposals/{order_uid}` | `X-Signature` | List your proposals on one order. | +| `GET` | `/proposals/by-sub-solver` | `X-Signature` | List all your proposals. | +| `DELETE` | `/proposal/{id}` | `X-Signature` (EIP-712 `CancelProposal`) | Cancel a proposal. Works only on `Submitted` or `Active` proposals. | -Poll for the verdict. Reads are signature-gated and scoped to you — you sign a long-lived read token once and send it on every request, and you cannot see anyone else's proposals on an order, not even the fact that they exist. A proposal that is not yours returns 404 rather than 403, so do not read a 404 as "deleted". +### Read authentication -Expect a verdict within roughly one block, bounded by the validator tick rather than by your request. Latency budgets are in [SLO targets](../operations/slo-targets). +Sign an EIP-712 `ReadAuth { version: 1 }` message once. Send it in the `X-Signature` header with every `GET` request. This signature has no timestamp or nonce. If it leaks, the risk is limited to read access to your own proposals. The signature does not grant write or cancellation access. -Then keep polling. A live proposal is re-simulated every tick and can die at any point because the chain moved. Run a loop: quote, sign, submit, watch, resubmit. That loop is the intended operating mode, and several design decisions assume you have one. +If you query a proposal that is not yours, you get `404` (not `403`). You cannot check if a proposal id exists. -To withdraw a proposal before it settles, send a signed cancellation. Proposals are immutable, so there is no update — replace by cancelling and posting a new one. +### The response to POST is not acceptance -## 7. When things go wrong +A `2xx` response means "accepted for validation". BYOS stores the proposal as `Submitted` and returns an id. Escrow checks and simulation run in a background loop ([`#proposal-api`](../design-document#proposal-api)). Do not treat a `2xx` as "my proposal is live". -| What happened | What it costs you | What to do | +### Poll for the verdict + +After you submit, poll for the verdict with `GET /proposal/{id}`. You can see only your own proposals. Expect a verdict within approximately one block. The validator tick interval determines the latency, not the request round-trip. See [SLO targets](../operations/slo-targets). + +Continue to poll after the first verdict. A live proposal is re-simulated every tick. It can fail at any time because chain state changed. + +Run a loop: quote, sign, submit, poll, resubmit. This loop is the intended operating mode. + +### Cancel a proposal + +To cancel a proposal before it settles, send a signed `DELETE` request. Proposals are immutable. There is no update operation. To replace a proposal, cancel it and submit a new one. + +## 7. Error handling + +| What happened | Cost | Action | |---|---|---| -| Rejected at gatekeeping | nothing | Read the typed reason and fix the route or the amounts. | -| Simulation reverted | nothing beyond a rate-limit slot | The proposal is dropped permanently on the first revert, with no retries. Resubmit if you still believe in the route — resubmission is how you say so. | -| Expired | nothing | Your `validUntil` passed. Shorten your loop. | -| Lost the auction | nothing | Normal. Your proposal stays live and competes again next auction. | -| Settlement reverted on-chain | a [Track A](../design-document#track-a) debit | Debited immediately. You have a dispute window on narrow, verifiable grounds. | -| BYOS won and did not settle | a smaller Track A debit | Same window, same grounds. | -| CoW raised an EBBO or fairness claim | a [Track B](../design-document#track-b) passthrough | Your balance is frozen on receipt and you get the certificate and evidence. Your refutation window is tight — see below. | +| Rejected at gatekeeping | None | Read the typed rejection reason. Fix the route or the amounts. | +| Simulation reverted | None (one rate-limit slot used) | The proposal is dropped on the first revert. There are no retries. Resubmit if the route is still valid. | +| Expired | None | Your `validUntil` passed. Use a shorter interval. | +| Lost the auction | None | Your proposal stays live and competes in the next auction. | +| Settlement reverted on-chain | [Track A](../design-document#track-a) debit | BYOS debits your escrow immediately. You have a 72-hour dispute window. | +| BYOS won but did not settle | Smaller Track A debit | Same dispute window and grounds. | +| CoW raised an EBBO or fairness claim | [Track B](../design-document#track-b) passthrough | BYOS freezes your balance and sends you the certificate and evidence. | + +### Simulation failures + +Simulation failures do not cost escrow. Only on-chain failures cause escrow debits. If a revert is caused by BYOS's own orchestration (not your route), BYOS pays. -Simulation failures are free on purpose. They are usually environmental — a pool moved, the order filled elsewhere — and charging for them would punish honest participants. Only on-chain failures touch your escrow. +### Track B operational readiness -Reverts caused by BYOS's own orchestration rather than your route are BYOS's cost, not yours. +Track B claims need operational readiness. Claims can arrive up to three months after the trade. Your refutation window is 36 hours. The CoW core team arbitrates (not BYOS). BYOS cannot fabricate a claim against you, but it also cannot waive one. -Track B is the one that needs operational readiness. Claims can arrive up to three months after the trade, your refutation window inside that is short, and the arbiter is the CoW core team rather than BYOS — so BYOS cannot fabricate a claim against you, but it also cannot waive one. If you cannot respond to evidence requests within a day and a half, that is a real risk to price in. +If you cannot respond to evidence requests within 36 hours, this is a risk you must plan for. -Every penalty action emits an on-chain Escrow event. That is the public record, and it is enough for you to audit your own history without trusting BYOS's private accounting. +Every penalty action emits an on-chain Escrow event. You can use these events to audit your own history. -## 8. A worked example +## 8. Reference implementations + +Two baseline sub-solver examples exist. Both do the full loop: fetch orders, compute a Uniswap V2 route, sign an EIP-712 proposal, submit, poll, and resubmit. + +| Language | Location | Notes | +|---|---|---| +| **Rust** | [`crates/subsolver`](https://github.com/bleu/byos-service/tree/main/crates/subsolver) in `byos-service` | Used in the Rust service's end-to-end test suite. | +| **TypeScript** | [`apps/subsolver`](https://github.com/bleu/byos-service-ts/tree/main/apps/subsolver) in `byos-service-ts` | Uses viem for EIP-712 signing. Uses multicall for reserve fetching. | -The reference sub-solver in [`crates/subsolver`](https://github.com/bleu/byos-service/tree/main/crates/subsolver) is a working client and the counterpart in the end-to-end test suite. It does the whole loop: fetch orders, quote a baseline route, sign, submit, poll, resubmit. +The protocol is language-neutral. Use either example for the sequence, the EIP-712 construction, and the polling behavior. The [OpenAPI document](https://github.com/bleu/byos-service/blob/main/crates/byos/openapi.yml) specifies all wire-level details. -It is Rust, but the protocol is language-neutral — what you want from it is the sequence, the EIP-712 construction, and the polling behaviour. Everything it does over the wire is specified in the OpenAPI document. +## Pre-launch checklist -## Checklist before you go live +Before you go live, make sure that: -- Escrow funded above the minimum, and your Trampoline deployed (a deposit does both). -- EIP-712 hashes verified against contract-provided vectors, not your own re-derivation. -- Domain configuration pinned to the right chain and contracts generation. -- `validUntil` inside the ingestion cap. -- Route leaves headroom above the user's limit for the gas cut and the driver's fee shift. -- Required order hooks included in your interactions. -- A polling loop that resubmits, rather than fire-and-forget submission. -- An operational path for responding to a Track B claim inside its window. +- [ ] You funded the Escrow above the minimum. A deposit also deploys your Trampoline. +- [ ] You verified your EIP-712 hashes against the contract's test vectors (not your own derivation). +- [ ] Your domain configuration points to the correct chain and contracts generation. +- [ ] Your `validUntil` value is within the ingestion cap. +- [ ] Your route leaves headroom above the user's limit for the gas cut and the driver's fee shift. +- [ ] You have a polling loop that resubmits (not fire-and-forget). +- [ ] You have an operational process to respond to a Track B claim within 36 hours. diff --git a/operations/slo-targets.md b/operations/slo-targets.md index 43723c6..8735a79 100644 --- a/operations/slo-targets.md +++ b/operations/slo-targets.md @@ -1,6 +1,6 @@ # SLO targets -Latency targets for the BYOS service, and the reasoning behind each number. These are commitments the service implementations are built against, so they belong here rather than in either service repo. +Latency targets for the BYOS service, and the reasoning behind each number. ## `POST /solve` p99 < 100ms @@ -8,7 +8,7 @@ The hot path, called by the CoW driver during auctions. The driver gives solvers a 15-second deadline, configurable via `solve_deadline` in the autopilot. BYOS does no simulation and no RPC on this path: an indexed read of the live proposal rows per auction order, one `solutions` insert per returned bid, and scoring and encoding in memory ([`#solver-engine`](../design-document#solver-engine)). -100ms is conservative against a 15s deadline. The point is not to be fast, it is to guarantee BYOS is never the bottleneck in the auction cycle. +100ms is conservative against a 15s deadline — BYOS should never be the bottleneck in the auction cycle. ## `GET /proposals/by-sub-solver` p99 < 50ms diff --git a/overview.md b/overview.md new file mode 100644 index 0000000..d700460 --- /dev/null +++ b/overview.md @@ -0,0 +1,135 @@ +# What is BYOS + +BYOS (Bring Your Own Solver) is a bonded CoW Protocol solver that opens CoW's order flow to **permissionless external routers** — called sub-solvers — without requiring them to go through the protocol's standard solver onboarding. + +## The problem BYOS solves + +Becoming a CoW solver today is a gated process: + +| Requirement | Standard pool (CIP-7) | Reduced pool (CIP-44) | +|---|---|---| +| Capital | $500,000 in stablecoins + 1,500,000 COW | $50,000–$100,000 + 500,000–1,000,000 COW | +| Governance | Deploy a Gnosis Safe with CoW DAO as sole signer | Same Safe requirement | +| Vouching | Vouched by an existing solver or the DAO | Core-team approval required | +| Onboarding | Shadow competition and testing on Sepolia before mainnet access | Same requirement | +| Compliance | KYC through the vouching solver's pool | Same | + +A DEX aggregator, a routing API, or an independent quant who can find good routes has no way to participate without first finding a bonding pool willing to vouch for them and locking up significant capital. + +**After BYOS**, the barrier drops to a collateral deposit sized to cover one worst-case revert penalty (`gas + c_l`, where `c_l` is 0.010 ETH on mainnet) and the ability to sign an EIP-712 message and return a route. + +## How it works + +BYOS sits between sub-solvers and the CoW auction as a single bonded solver. From the protocol's perspective it is an ordinary solver. Internally, it sources its solutions from anyone willing to post collateral. + +```mermaid +flowchart LR + subgraph Sub-solvers + S1[Sub-solver A] + S2[Sub-solver B] + S3[Sub-solver C] + end + + subgraph BYOS + API[Proposal API] + V[Validator] + E[Solver engine] + end + + subgraph CoW Protocol + D[Driver] + A[Auction] + end + + S1 & S2 & S3 -->|signed proposals| API + API --> V + V -->|active proposals| E + D -->|/solve| E + E -->|best solution per order| D + D --> A +``` + +The flow: + +1. **Sub-solvers find orders** in CoW's public orderbook and compute routes using any DEX or protocol. +2. **Sub-solvers sign and submit proposals** — EIP-712 messages committing to a specific order, route, and minimum output (`buyAmount` floor). +3. **BYOS validates** each proposal in the background: checks escrow balance, simulates the full settlement via `eth_estimateGas`, and scores it (`surplus - gas`). +4. **When the CoW driver calls `/solve`**, BYOS answers instantly from its pool of validated proposals — no RPC, no simulation on the hot path. It picks the highest-scoring proposal per order. +5. **The driver settles** the winning solution on-chain. The sub-solver's route executes inside a per-sub-solver sandbox contract (the Trampoline), isolated from settlement buffers. +6. **If the settlement reverts**, BYOS debits the sub-solver's escrow for `gas + c_l` (gas cost plus the per-auction lower reward cap). + +## What sub-solvers get and don't get + +| | Sub-solver | BYOS | +|---|---|---| +| **Route computation** | Responsible | Not involved | +| **Transaction submission** | Not involved | Responsible (via CoW driver) | +| **Scoring and auction bidding** | Not involved | Responsible | +| **Revenue from own venue fees** | Keeps any fees their route earns at the DEX level (e.g., LP fees on a pool they operate) | Not involved | +| **In-route surplus capture** | May capture surplus inside the route before the sweep ([details](design-document#residue)) | Keeps uncaptured surplus as settlement slippage | +| **Gas cut** | Not charged directly | Retains the estimated gas cost on every settled trade ([details](design-document#gas)) | +| **CoW solver rewards** | None in v1 — no reward pass-through | Retains 100% of CoW rewards earned under its bonded solver seat | +| **Escrow risk** | Bears Track A (revert) and Track B (EBBO) penalties | Absorbs shortfall when escrow is insufficient | + +## How BYOS earns revenue + +BYOS keeps the **gas cut** — the estimated gas cost of each settlement, denominated in the order's sell token. This is declared as a solver fee in the solution — a price wedge, not a deduction from the route. The difference stays in `GPv2Settlement`'s buffers and returns to BYOS via the weekly settlement payout. The protocol does not reimburse gas; what returns weekly is revenue BYOS retained from the trade. + +Only settled trades generate revenue. + +Additionally, BYOS retains all **CoW solver rewards** (CIP-20/CIP-85 performance and consistency rewards) earned under its bonded solver address. Reward pass-through to sub-solvers is out of scope for v1. + +## Why proposals get discarded + +A proposal can be rejected at multiple stages. Here is every reason, consolidated: + +### At submission (synchronous, immediate 4xx) + +| Reason | Meaning | +|---|---| +| Invalid signature | Malformed signature hex or recovery failure | +| Proposal expired | `validUntil` is already in the past | +| Lifetime exceeded | `validUntil` is more than 5 minutes in the future (configurable) | +| Rate limited | IP or signer rate limit exceeded | + +### At validation (asynchronous, recorded on the proposal) + +| Reason | Meaning | +|---|---| +| Insufficient escrow | Balance below the threshold (`gas estimate × gas price + minimum collateral`) | +| Order not found | Order UID not in CoW's orderbook (filled, expired, or cancelled) | +| Unsupported order | Non-ERC20 balance flavors, bridging orders, or (in v1) partially fillable orders | +| Amount mismatch | Proposal amounts don't match the order (fill-or-kill mismatch, or partial fill violates limits) | +| Unprofitable | Score (`surplus - gas`) is zero or negative on first simulation | +| Simulation failed | The full settlement simulation reverted — terminal on first occurrence, no retries | + +### By lifecycle (not a rejection, but the proposal stops competing) + +| State | Cause | +|---|---| +| Expired | `validUntil` passed | +| Cancelled | Sub-solver sent a signed `DELETE` | +| Settled | The proposal won an auction and settled on-chain | +| Settle failed | Settlement reverted on-chain — triggers a Track A penalty | + +## The slashing policy + +When a settlement carrying a sub-solver's route causes BYOS to incur a cost from CoW, BYOS recovers it from the sub-solver's escrow. There are two tracks: + +**Track A — routine, fast, provable.** Covers reverts, deadline misses, and non-settlements. BYOS debits immediately with no prior approval, because the facts are on-chain and verifiable by anyone. The sub-solver gets a 72-hour dispute window on narrow grounds (wrong attribution, the transaction didn't actually revert, the amount exceeds the cap). + +| Scenario | Debit amount | +|---|---| +| Settlement reverts on-chain | `gas used + c_l` | +| Settlement misses block deadline | `gas used + c_l` | +| Won auction, never settled | `10% of c_l` | + +`c_l` is CoW's per-auction lower reward cap: **0.010 ETH** on Ethereum mainnet, **10 xDAI** on Gnosis. + +**Track B — rare, slow, externally arbitrated.** Covers EBBO (Execution-Based Best Offer) violations and catch-all fairness claims. CoW's core team issues a certificate against BYOS; BYOS identifies the responsible sub-solver, freezes their escrow to block withdrawal, and gives them 36 hours to submit a refutation. The CoW core team — not BYOS — adjudicates. This means BYOS cannot fabricate a Track B claim, but it also cannot waive one. + +Track B claims can arrive up to **three months** after the trade. If the sub-solver has already withdrawn or the escrow balance is insufficient, BYOS absorbs the difference. + +**Simulation failures are free.** Only on-chain failures touch escrow. A proposal that fails simulation is dropped with no penalty. + +For the full normative specification, see [the design document](design-document#penalties). diff --git a/reference/cow-fee-collection.md b/reference/cow-fee-collection.md index 337a2ac..24bed2d 100644 --- a/reference/cow-fee-collection.md +++ b/reference/cow-fee-collection.md @@ -98,7 +98,7 @@ sequenceDiagram W->>D: solver payout in native token
(or overdraft if negative) ``` -Two consequences of the score formula worth remembering: +Two consequences of the score formula: - Ranking is fee-neutral. Score counts protocol fees *as if collected*, computed by the autopilot from the executed amounts. A solver that skips the fee gives the user more surplus but the score @@ -160,8 +160,7 @@ flowchart TD The model is trust-minimized, not trustless: it works because the fee debt is deterministically computable from on-chain data, and because a bonded solver has more at stake (bond + future -revenue) than any single week's shortfall. A solver that walks away is recoverable only up to its -bond — which is why solver onboarding is permissioned. +revenue) than any single week's shortfall. A solver that walks away is recoverable only up to its bond. ## Who does what — summary diff --git a/reference/solver-auctions.md b/reference/solver-auctions.md index bc432cd..2762efd 100644 --- a/reference/solver-auctions.md +++ b/reference/solver-auctions.md @@ -3,7 +3,7 @@ > Consolidated from the official docs under . > Captured 2026-06-18 for offline consultation while exploring the BYOS RFP ([BYOS RFP](https://forum.cow.fi/t/rfp-bring-your-own-solver-byos/3469)). For authoritative/current text, follow the source link in each section. > -> Why this matters for BYOS: BYOS is a bonded solver that must win the standard CoW auction. Everything below — how solutions are scored, how the fair combinatorial auction picks winners, EBBO, rewards, accounting, and bonding — applies to BYOS itself. Sub-solver proposals must ultimately produce a *valid, competitive* CoW solution under these rules. +> Why this matters for BYOS: BYOS is a bonded solver that must win the standard CoW auction. Sub-solver proposals must produce a valid, competitive CoW solution under these rules. CoW Protocol uses an implementation of the [Fair Combinatorial Auction](https://arxiv.org/abs/2408.12225) (FCA) to execute trades. A **solver** is an algorithm that takes an auction instance (valid orders, liquidity state, protocol rules/fees) and outputs one or more **solutions** selecting order subsets and feasible amounts. diff --git a/security/trampoline-settlement-isolation.md b/security/trampoline-settlement-isolation.md index 1ffab8b..c5a3d3e 100644 --- a/security/trampoline-settlement-isolation.md +++ b/security/trampoline-settlement-isolation.md @@ -16,12 +16,10 @@ settlement ([`#residue`](../design-document#residue)), so the instance is empty trade tokens at rest, and each sub-solver has a distinct instance. The blast radius of any route is the trade capital in flight during its own settlement. -The tests demonstrate this against the **real deployed `GPv2Settlement`** on a mainnet -fork, not a mock — the point is to exercise CoW's actual semantics (allowance checks, -`onlySolver`, the reentrancy guard, owner-scoped order state), since a mock would only -restate our own assumptions. A controlled ERC-20 buffer is seeded into the settlement in -`setUp`, so every "no value moved" assertion runs against real, non-zero value rather -than a vacuous zero-to-zero. +The tests run against the **real deployed `GPv2Settlement`** on a mainnet fork, exercising +CoW's actual semantics (allowance checks, `onlySolver`, the reentrancy guard, +owner-scoped order state). A controlled ERC-20 buffer is seeded into the settlement in +`setUp`, so every "no value moved" assertion runs against non-zero value. The invariant asserted is that value does not move — buffer balances, allowances, and order state are unchanged after the route runs. A revert is one mechanism that enforces diff --git a/service.md b/service.md new file mode 100644 index 0000000..5349e99 --- /dev/null +++ b/service.md @@ -0,0 +1,202 @@ +# Service architecture + +The off-chain component of BYOS: a single-process service that ingests sub-solver proposals, validates them against the chain, scores them, and answers the CoW driver's `/solve` endpoint with candidate solutions. + +Two implementations exist — a [Rust service](https://github.com/bleu/byos-service) and a [TypeScript migration](https://github.com/bleu/byos-service-ts). This page describes the shared architecture; implementation-specific ADRs live in each repo. + +## Two-listener model + +The service binds two ports on one process, with opposite trust boundaries: + +| Listener | Default port | Serves | Trust level | +|---|---|---|---| +| **Public** | 9585 | `/proposals` (POST), `/proposal/:id` (GET, DELETE), `/proposals/by-sub-solver` (GET), `/proposals/:orderUid` (GET) | Internet-reachable | +| **Internal** | 9586 | `/solve`, `/notify` | Firewalled, driver-only | + +They never share a socket. A `/solve` response contains the full standing proposal book for an auction — amounts, routes, and signatures, all MEV-relevant — so it must not be reachable from the public internet. An optional bearer token on `/solve` provides defense in depth. + +## Request flow + +### Proposal ingestion (public) + +```mermaid +flowchart LR + SS[Sub-solver] -->|POST /proposals| PR[Parse + ecrecover] + PR --> EX[Expiry window check] + EX --> DB[(Store as Submitted)] + DB --> R[202 Accepted + id] +``` + +The request path does three things inline: parse, `ecrecover`, and expiry-window check. On success, the proposal is stored as `Submitted` and answered `202` — meaning "accepted for validation", not "accepted". + +**All on-chain work is deferred.** The escrow balance check and settlement simulation run in a background validator loop, not on the request path. This decouples response latency from blockchain health and prevents DDoS on the public port from starving `/solve`. + +### Solving (internal) + +```mermaid +flowchart LR + D[CoW Driver] -->|POST /solve
auction + gas price| E[Solver engine] + E --> F[Fetch active proposals
by order UIDs] + F --> S[Score: surplus - gas
per proposal] + S --> W[Pick highest per order] + W --> C[Build settlement
interactions] + C --> R[Return solutions] +``` + +`/solve` is the hot path — **no RPC, no simulation, no writes** beyond recording the solution-to-proposal mapping. It reads from Postgres (indexed by order UID), scores with cached gas estimates, computes CREATE2 addresses locally, and ABI-encodes the two interactions (transfer + execute). Target latency: p99 < 100ms. + +### Settlement outcomes (internal) + +The CoW driver notifies BYOS of outcomes via `POST /notify`. There is no chain watcher and no driver fork. + +| Notification | Proposal transition | +|---|---| +| `SettlementStarted` | `Active` → `Executing` | +| `Success { transaction }` | `Active` or `Executing` → `Settled` | +| `Revert { transaction }` | `Active` or `Executing` → `SettleFailed` (triggers Track A debit) | +| `Cancelled` / `Expired` / `Fail` | `Executing` → `Active` (queues non-settlement debit) | + +`/notify` joins to proposals through the `(auction_id, solution_id, proposal_id)` mapping that `/solve` records synchronously before returning solutions. + +## Background workers + +Three background loops run alongside the HTTP listeners: + +### Validation loop + +Runs every ~12 seconds (one block): + +1. **Release stale executing proposals** — proposals stuck in `Executing` for more than 5 minutes (lost notification or restart) fall back to `Active`. +2. **Expire proposals** — any `Submitted` or `Active` proposal with `validUntil < now` transitions to `Expired`. +3. **Validate remaining proposals** — for each `Submitted` or `Active` proposal: + - **Escrow check** (cheap): `effectiveBalance(subSolver) >= ESCROW_GAS_ESTIMATION × gas_price + min_collateral`, where `ESCROW_GAS_ESTIMATION` is a fixed 200k gas floor. Reject if insufficient. + - **Order envelope check** (no RPC): fill-or-kill amounts match, ERC20 balances only, no bridging orders. + - **Settlement simulation** (expensive): full `settle()` via `eth_estimateGas` with state overrides. Records gas used, trampoline address, and token addresses on success. + - **Profitability gate** (first validation only): `score = surplus - gas > 0`. Not re-applied on re-validation to avoid gas-price flapping churn. + +A simulation revert is **terminal on first occurrence** — no strikes, no retries. + +### Penalty loop + +Runs on the same interval as validation. Processes Track A debits: + +1. For each `SettleFailed` proposal: fetch settlement tx receipt, compute `gas_used × effective_gas_price + c_l`, call `escrow.debit(subSolver, amount, txHash)`. +2. For each pending non-settlement debit: call `escrow.debit(subSolver, 0.1 × c_l, orderUidHash)`. +3. On success: transition to `Penalized`, record the debit tx hash. +4. Retry up to 10 times on transient failures, then park for operator investigation. + +### Retention sweep + +Runs every ~5 minutes. Deletes terminal proposals (`Rejected`, `SimFailed`, `Expired`, `Cancelled`) that have been in their terminal state for more than 1 hour. Money states (`Settled`, `SettleFailed`, `Penalized`) are kept indefinitely — they are dispute evidence. + +## Proposal lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Submitted: POST /proposals + Submitted --> Active: simulation passes,\nscore > 0 + Submitted --> Rejected: gatekeeping fails + Submitted --> SimFailed: simulation reverts + Active --> Active: re-simulation each tick + Active --> SimFailed: re-simulation reverts + Active --> Rejected: escrow re-check fails + Active --> Executing: driver SettlementStarted + Submitted --> Expired: validUntil passed + Active --> Expired: validUntil passed + Submitted --> Cancelled: DELETE + Active --> Cancelled: DELETE + Active --> Settled: driver Success\n(missed Started) + Active --> SettleFailed: driver Revert\n(missed Started) + Executing --> Settled: driver Success + Executing --> SettleFailed: driver Revert + Executing --> Active: driver Cancelled/Fail,\nor timeout + SettleFailed --> Penalized: Track A debit lands +``` + +A state answers one question: **what does the service do with this proposal right now?** + +| State | Simulated? | Offered to `/solve`? | Cancellable? | +|---|---|---|---| +| `Submitted` | First pass pending | No | Yes | +| `Active` | Every tick | Yes | Yes | +| `Executing` | No | No | No | +| `Rejected` / `SimFailed` / `Expired` / `Cancelled` | No | No | No | +| `Settled` / `SettleFailed` / `Penalized` | No | No | No | + +Transitions are **compare-and-swap** — zero rows affected means the caller's verdict was stale (a cancellation or notification won the race). + +## Persistence + +Postgres is the source of truth: + +| Table | Purpose | Retention | +|---|---|---| +| `proposals` | Current state — read by `GET`, `/solve`, `/notify`, and the validator | Live proposals indefinite; terminal states swept after 1 hour (except money states) | +| `audit_events` | Append-only history — what happened, when, why | No deletion path. Dispute evidence for Track B claims arriving up to 3 months later. | +| `solutions` | Attribution mapping `(auction_id, solution_id) → proposal_id` | Indefinite | +| `penalties` | Pending non-settlement debits (queued by `Cancelled`/`Expired`/`Fail` notifications) | Processed by the penalty loop, then retained | + +The audit trail uses a write-behind pattern: events are emitted after their proposal write commits, persisted by a dedicated background worker. This decouples audit codec evolution from the store's hot path. The crash window (state change committed, audit event not yet persisted) is accepted at one event per crash. + +## Scoring + +``` +score = surplus - gas +``` + +- **Surplus**: improvement beyond the order's limit price (extra buy tokens on a sell order, sell tokens kept back on a buy order), converted at the auction's reference price. +- **Gas**: simulated `eth_estimateGas` result + 30k buffer, times the auction's effective gas price. + +There is no fee term. CoW's score is `surplus + protocol fees`, and the protocol fee cancels out of ranking. Once the gas cut equals the gas cost, `surplus - gas` matches what the autopilot computes. + +BYOS's score is a **pre-ranking** that decides which proposals deserve the driver's encoding budget. The driver re-scores after encoding and simulation. + +## Gas cut + +BYOS keeps the estimated gas cost of each settlement, in the order's sell token, as its revenue. It is declared as the fulfillment's `fee` field — a price wedge, not a deduction from the route. + +``` +effective_gas = gas_used + 30k buffer +cut_in_wei = effective_gas × effective_gas_price +cut_in_sell_tokens = ceil(cut_in_wei × 10^18 / sell_token_price) +``` + +The cut is **not padded** — a larger cut lowers the score, which lowers CIP-85 consistency rewards. A proposal is skipped if the cut would breach the user's signed limit. + +## Key design decisions + +Rationale for each decision lives in the ADRs of [byos-service](https://github.com/bleu/byos-service/tree/main/docs/adr) and [byos-service-ts](https://github.com/bleu/byos-service-ts/tree/main/docs/adr). This section summarizes the final state. + +### Async ingestion + +The request path does only signature + expiry checks. Escrow and simulation validation run in the background. + +A `2xx` from `POST /proposals` means "accepted for validation", not "accepted". + +### No chain watcher + +Settlement outcomes come from the stock CoW driver's `/notify` endpoint, not from scanning blocks. This covers private submissions and dropped transactions that a block scanner would miss. Missed-deadline detection comes free from `Cancelled` and `Expired` notifications. + +### Owner-scoped reads + +All `GET` endpoints require an EIP-712 signature, and the recovered signer scopes the response. Non-owners get `404`, not `403`, to prevent existence-oracle attacks. + +### First-revert-terminal simulation + +A proposal that fails simulation once is not re-simulated. + +### Profitability gate on first validation only + +A score of zero or less rejects as `Unprofitable` on the first simulation, matching `/solve`'s inclusion rule. It is not re-applied on re-validation — gas prices wobble, and rejecting on a spike would churn proposals that are profitable again two blocks later. + +### Proposal lifetime cap + +`validUntil` more than 5 minutes in the future is rejected at ingestion. This bounds worst-case simulation cost per proposal and guarantees the expiry sweep arrives within a known window. + +### One sub-solver per settlement transaction + +The per-sub-solver Trampoline CREATE2 address in the calldata identifies which sub-solver's route ran. The driver's `SolutionMerging` is set to `Forbidden` to prevent silent batching. + +### Compare-and-swap transitions + +All state transitions check the expected current state before updating. Zero rows affected means a concurrent transition won — a cancellation, a notification, or an expiry sweep raced and won. No stale overwrites.