|
| 1 | +# fastswap-miles |
| 2 | + |
| 3 | +Indexes `IntentExecuted` events from the FastSettlementV3 contract, calculates net profit per swap, and awards 90% of profit as miles to users via the Fuel API. |
| 4 | + |
| 5 | +## Flow |
| 6 | + |
| 7 | +```mermaid |
| 8 | +flowchart TD |
| 9 | + A["Swap via FastRPC"] --> B["IntentExecuted event"] |
| 10 | + B --> C["Index into DB"] |
| 11 | + C --> V["In mctransactions_sr?"] |
| 12 | + V -->|No| W["Skip, 0 miles"] |
| 13 | + V -->|Yes| X["Bid cost from tx_view"] |
| 14 | + X --> D{"swap_type?"} |
| 15 | +
|
| 16 | + D -->|"token→ETH"| E["surplus - gas - bid = net"] |
| 17 | + D -->|"ETH→token"| F["Batch by token"] |
| 18 | +
|
| 19 | + F --> K["Barter sweep quote"] |
| 20 | + K -->|Not profitable| M["Leave unprocessed"] |
| 21 | + K -->|Profitable| L["share - bid - overhead"] |
| 22 | +
|
| 23 | + E --> J{"net > 0?"} |
| 24 | + L --> J |
| 25 | +
|
| 26 | + J -->|No| N["0 miles"] |
| 27 | + J -->|Yes| O["90% net → miles"] |
| 28 | + O --> P["Submit to Fuel"] |
| 29 | +``` |
| 30 | + |
| 31 | +### Main Loop |
| 32 | + |
| 33 | +1. **Index** — Polls L1 in batches (`-batch`, default 2000 blocks), filters for `IntentExecuted` events from the FastSettlementV3 contract, and inserts into StarRocks (`fastswap_miles` table). |
| 34 | +2. **Process** — Once caught up to chain tip, processes all unprocessed rows: |
| 35 | + - **Token→ETH swaps** (swap_type=`eth_weth`): surplus is already in ETH, calculate profit immediately. |
| 36 | + - **ETH→Token swaps** (swap_type=`erc20`): accumulated token surplus is batched and swept to ETH via FastSwap. |
| 37 | +3. **Award** — 90% of net profit → miles, submitted to Fuel API. Row marked `processed = true`. |
| 38 | + |
| 39 | +### Profit Calculation |
| 40 | + |
| 41 | +**Token→ETH** (processed immediately): |
| 42 | +``` |
| 43 | +net_profit = surplus - gas_cost - bid_cost |
| 44 | +``` |
| 45 | +- `gas_cost` = `receipt.GasUsed × receipt.EffectiveGasPrice` (we pay gas for token-input swaps) |
| 46 | +- `bid_cost` from most recent `OpenedCommitmentStored` event in `tx_view` (ordered by block number DESC) |
| 47 | +- Gas cost is **zeroed** for ETH-input swaps (user pays gas) |
| 48 | + |
| 49 | +**ETH→Token** (batched, swept when profitable): |
| 50 | +``` |
| 51 | +sweep_return = Barter quote for all accumulated surplus of that token |
| 52 | +per_user_return = sweep_return × (user_surplus / total_surplus) |
| 53 | +net_profit = per_user_return - bid_cost - proportional_gas_overhead |
| 54 | +``` |
| 55 | + |
| 56 | +### FastSwap Sweep (ERC20 → ETH) |
| 57 | + |
| 58 | +When ERC20 surplus accumulates from ETH→Token swaps, the service periodically checks if sweeping those tokens to ETH is profitable. It does this by running a dry-run test swap against the Barter API: |
| 59 | + |
| 60 | +1. **Batching**: Groups all unprocessed `erc20` swaps by their output token. |
| 61 | +2. **Quote**: Gets a Barter swap quote (2% slippage) to sell the *total sum* of the accumulated token surplus for ETH. |
| 62 | +3. **Profitability Check**: |
| 63 | + - Calculates the net return in ETH (`quote.AmountOut`). |
| 64 | + - Slices the proportional Barter sweep execution gas cost per user. |
| 65 | + - If `user_sweep_return > bid_cost + gas_overhead`, the user is profitable. The sweep executes if the batch overall has a positive net profit. |
| 66 | +4. **Execution**: If profitable, signs a Permit2 EIP-712 `PermitWitnessTransferFrom` with the Intent as witness, and POSTs to the `/fastswap` endpoint. `userAmtOut` is set to 95% of Barter's `minReturn` to allow for price movement between quote and execution. |
| 67 | +5. **Distribution**: The point value of the swept ETH is divided proportionally among the users in the batch for accounting purposes. 90% of each user's calculated net profit is awarded to them as miles. |
| 68 | + |
| 69 | +**Executor exclusion**: The executor/treasury address is filtered out (`WHERE user_address != executor`) so sweep transactions don't earn miles. |
| 70 | + |
| 71 | +## Database |
| 72 | + |
| 73 | +**StarRocks** — `mevcommit_57173.fastswap_miles` |
| 74 | + |
| 75 | +| Column | Type | Description | |
| 76 | +|---|---|---| |
| 77 | +| `tx_hash` | VARCHAR(100) PK | L1 transaction hash | |
| 78 | +| `block_number` | BIGINT | L1 block number | |
| 79 | +| `block_timestamp` | DATETIME | Block timestamp | |
| 80 | +| `user_address` | VARCHAR(64) | Swap initiator | |
| 81 | +| `input_token` | VARCHAR(64) | Token sent by user | |
| 82 | +| `output_token` | VARCHAR(64) | Token received by user | |
| 83 | +| `surplus` | VARCHAR(100) | Executor surplus (raw wei) | |
| 84 | +| `gas_cost` | VARCHAR(100) | L1 gas cost (wei) | |
| 85 | +| `bid_cost` | VARCHAR(100) | mev-commit bid cost (wei) | |
| 86 | +| `swap_type` | VARCHAR(16) | `eth_weth` or `erc20` | |
| 87 | +| `surplus_eth` | DOUBLE | Surplus in ETH | |
| 88 | +| `net_profit_eth` | DOUBLE | Net profit after costs | |
| 89 | +| `miles` | BIGINT | Miles awarded | |
| 90 | +| `processed` | BOOLEAN | `false` = not yet awarded | |
| 91 | + |
| 92 | +## CLI Flags |
| 93 | + |
| 94 | +### Production Mode |
| 95 | + |
| 96 | +```bash |
| 97 | +go run ./tools/fastswap-miles/ \ |
| 98 | + -l1-rpc-url $L1_RPC_URL \ |
| 99 | + -keystore /path/to/keystore.json \ |
| 100 | + -passphrase $KEYSTORE_PASSWORD \ |
| 101 | + -barter-url $BARTER_URL \ |
| 102 | + -barter-api-key $BARTER_KEY \ |
| 103 | + -fuel-api-url $FUEL_URL \ |
| 104 | + -fuel-api-key $FUEL_KEY \ |
| 105 | + -fastswap-url "https://fastrpc.mev-commit.xyz" \ |
| 106 | + -funds-recipient "0x..." \ |
| 107 | + -db-user $DB_USER \ |
| 108 | + -db-pw $DB_PW \ |
| 109 | + -db-host $DB_HOST \ |
| 110 | + -start-block 21781670 |
| 111 | +``` |
| 112 | + |
| 113 | +### Dry-Run Mode |
| 114 | + |
| 115 | +Indexes events and computes miles but **skips** Fuel submission and processed marking. Rows remain `processed = false` so the real service will pick them up. |
| 116 | + |
| 117 | +```bash |
| 118 | +go run ./tools/fastswap-miles/ \ |
| 119 | + -dry-run \ |
| 120 | + -l1-rpc-url $L1_RPC_URL \ |
| 121 | + -barter-url $BARTER_URL \ |
| 122 | + -barter-api-key $BARTER_KEY \ |
| 123 | + -db-user $DB_USER \ |
| 124 | + -db-pw $DB_PW \ |
| 125 | + -db-host $DB_HOST \ |
| 126 | + -start-block 21781670 |
| 127 | +``` |
| 128 | + |
| 129 | +### All Flags |
| 130 | + |
| 131 | +| Flag | Default | Description | |
| 132 | +|---|---|---| |
| 133 | +| `-l1-rpc-url` | (required) | L1 Ethereum HTTP RPC URL | |
| 134 | +| `-keystore` | | Path to executor keystore JSON file | |
| 135 | +| `-passphrase` | | Keystore password | |
| 136 | +| `-barter-url` | (required) | Barter API base URL | |
| 137 | +| `-barter-api-key` | | Barter API key | |
| 138 | +| `-fuel-api-url` | | Fuel points API URL (required in production) | |
| 139 | +| `-fuel-api-key` | | Fuel points API key (required in production) | |
| 140 | +| `-fastswap-url` | | FastSwap API endpoint (e.g., `https://fastrpc.mev-commit.xyz`) | |
| 141 | +| `-funds-recipient` | `0xD588...` | Address to receive swept ETH | |
| 142 | +| `-max-gas-gwei` | `50` | Skip sweep if L1 gas exceeds this | |
| 143 | +| `-contract` | `0x084C...` | FastSettlementV3 proxy address | |
| 144 | +| `-weth` | `0xC02a...` | WETH contract address | |
| 145 | +| `-start-block` | `0` | Block to start indexing (0 = resume from DB) | |
| 146 | +| `-poll` | `12s` | Poll interval for new blocks | |
| 147 | +| `-batch` | `2000` | Blocks per `eth_getLogs` batch | |
| 148 | +| `-dry-run` | `false` | Compute miles without submitting or marking | |
| 149 | +| `-log-fmt` | `json` | Log format (`text` or `json`) | |
| 150 | +| `-log-level` | `info` | Log level (`debug`, `info`, `warn`, `error`) | |
| 151 | +| `-log-tags` | | Comma-separated `name:value` pairs for log lines | |
| 152 | +| `-db-user` | | StarRocks user | |
| 153 | +| `-db-pw` | | StarRocks password | |
| 154 | +| `-db-host` | `127.0.0.1` | StarRocks host | |
| 155 | +| `-db-port` | `9030` | StarRocks port | |
| 156 | +| `-db-name` | `mevcommit_57173` | StarRocks database | |
| 157 | + |
| 158 | +## File Structure |
| 159 | + |
| 160 | +| File | Purpose | |
| 161 | +|---|---| |
| 162 | +| `main.go` | CLI, main loop, DB helpers, Barter/Fuel API clients, utility functions | |
| 163 | +| `miles.go` | `serviceConfig`, `processMiles`, `processERC20Miles`, bid cost lookups | |
| 164 | +| `sweep.go` | Event indexer, token sweep, Permit2 approval, EIP-712 signing | |
| 165 | +| `main_test.go` | Unit tests for miles calculation, API clients, helpers | |
| 166 | + |
| 167 | +## Key Behaviors |
| 168 | + |
| 169 | +- **Auto-resume**: If `-start-block` is 0, resumes from last saved block in `fastswap_miles_meta`. |
| 170 | +- **Permit2 auto-approval**: If the executor hasn't approved a token to Permit2, automatically sends a max-uint256 approval before sweeping. 15-minute receipt timeout. |
| 171 | +- **FastRPC check**: Only transactions found in `mctransactions_sr` get miles (filters out non-FastRPC swaps). |
| 172 | +- **Bid cost dedup**: When multiple providers commit to the same tx, uses the most recent `OpenedCommitmentStored` event (by block number). |
| 173 | +- **Dry-run safety**: In dry-run mode, rows are never marked as processed and no Fuel submissions are made. |
| 174 | +- **Caught-up guard**: Miles are only processed after the indexer has caught up to the chain tip, avoiding excessive Barter API calls during historical backfill. |
| 175 | +- **Graceful shutdown**: Catches SIGINT/SIGTERM, finishes current batch, then exits. |
| 176 | +- **Idempotent**: Re-running from the same start block is safe — inserts use `INSERT INTO` with primary key dedup. |
0 commit comments