Skip to content

Commit f78fb5d

Browse files
authored
feat: fastswap miles and token sweeping (#897)
* feat: fastswap miles and token sweeping * fix: remove unused func ensureSchema * test: add fastswap-miles unit tests * fix lint err in test * refactor: split fastswap-miles into 3 files, fix build errors and dry-run behavior - Split 1620-line main.go into main.go, miles.go, sweep.go - Fix dry-run to never mark rows as processed - Remove double processERC20Miles call - Remove dead testswap code and CLI flags - Migrate all logging to slog - Fix nondeterministic bid cost lookup (ORDER BY block DESC, take most recent) - Only process miles when caught up to chain tip - Add comprehensive unit tests for miles calculation - Update README to reflect new structure and behaviors
1 parent 9f6adbd commit f78fb5d

5 files changed

Lines changed: 2094 additions & 0 deletions

File tree

tools/fastswap-miles/README.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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

Comments
 (0)