|
| 1 | +# MEV Threat Model for SubTrackr Subscription Charges |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Maximal Extractable Value (MEV) in the context of Soroban subscription |
| 6 | +charges refers to the ability of validators, sequencers, or bots to |
| 7 | +reorder, include, or front-run charge transactions to extract value from |
| 8 | +subscribers. This document catalogues the threats and describes the |
| 9 | +protections implemented. |
| 10 | + |
| 11 | +## Threat Categories |
| 12 | + |
| 13 | +### 1. Front-running |
| 14 | + |
| 15 | +| Threat | Description | Severity | |
| 16 | +|--------|-------------|----------| |
| 17 | +| **Price Oracle Front-run** | An adversary observes a pending charge tx and submits their own tx with a manipulated oracle price before the charge is confirmed, causing the subscriber to overpay. | High | |
| 18 | +| **Insertion Front-run** | Validator inserts their own transfer before the subscriber's charge, draining the subscriber's token balance and causing the charge to fail (DoS). | Medium | |
| 19 | + |
| 20 | +**Mitigations:** |
| 21 | + |
| 22 | +- **Commit-Reveal** (`commit_charge` / `reveal_charge`): The subscriber |
| 23 | + commits to a SHA-256 hash of (amount, nonce) before the actual charge |
| 24 | + is executed. The price is hidden until `reveal_charge`, preventing |
| 25 | + front-runners from knowing the charge amount. |
| 26 | +- **Oracle Slippage Protection** (`PriceBounds` / `resolve_charge_price`): |
| 27 | + The resolved charge price is clamped to `[min_price_bps, max_price_bps]` |
| 28 | + of the plan price, limiting the impact of oracle manipulation. |
| 29 | +- **Per-call `max_gas_fee`**: The subscriber sets a maximum acceptable |
| 30 | + base fee per call; if the ledger base fee exceeds this threshold at |
| 31 | + execution time, the charge is rejected. |
| 32 | + |
| 33 | +### 2. Sandwich Attacks |
| 34 | + |
| 35 | +| Threat | Description | Severity | |
| 36 | +|--------|-------------|----------| |
| 37 | +| **Oracle Sandwich** | Adversary manipulates the oracle price feed before and after the subscriber's charge, profiting from the price difference. | Medium | |
| 38 | + |
| 39 | +**Mitigations:** |
| 40 | + |
| 41 | +- `resolve_charge_price` uses the oracle's `get_price_with_cache` (TTL |
| 42 | + = 600 ledgers) which returns a cached price rather than a live feed, |
| 43 | + reducing the window for oracle sandwich attacks. |
| 44 | +- Slippage bounds (`PriceBounds`) cap the maximum deviation from the |
| 45 | + plan's base price. |
| 46 | + |
| 47 | +### 3. Time-bandit / Reorg Attacks |
| 48 | + |
| 49 | +| Threat | Description | Severity | |
| 50 | +|--------|-------------|----------| |
| 51 | +| **Ledger Reorg** | A validator rewinds the ledger state to re-execute a charge at a more favourable price, or to double-spend a commitment. | Low | |
| 52 | + |
| 53 | +**Mitigations:** |
| 54 | + |
| 55 | +- Commitments include a `deadline` timestamp. If the ledger timestamp |
| 56 | + regresses past the deadline, `reveal_charge` rejects the reveal. |
| 57 | +- Commitments are single-use: `reveal_charge` removes the commitment |
| 58 | + from storage after successful execution, preventing replay. |
| 59 | + |
| 60 | +### 4. Gas Price Manipulation |
| 61 | + |
| 62 | +| Threat | Description | Severity | |
| 63 | +|--------|-------------|----------| |
| 64 | +| **Gas Price Spiking** | Validator raises the base fee to force subscribers into paying more gas than expected, or to extract rent from urgent charges. | Medium | |
| 65 | +| **Gas Griefing** | Adversary causes the charge transaction to consume more gas (e.g. by bloating storage reads) so the subscriber exceeds their gas budget. | Low | |
| 66 | + |
| 67 | +**Mitigations:** |
| 68 | + |
| 69 | +- **Per-subscription `MevChargeConfig.max_gas`**: Subscribers can set a |
| 70 | + hard cap on total gas per charge. If the actual gas used exceeds this |
| 71 | + cap, the transaction panics (and any partial state is rolled back). |
| 72 | +- **`GasPriceSnapshot`**: After each charge, a snapshot of (ledger_seq, |
| 73 | + base_fee, gas_used, amount_charged) is stored. Off-chain monitoring |
| 74 | + can detect abnormal gas price patterns. |
| 75 | +- **Per-call `max_gas_fee`**: Inline parameter on `charge_subscription` |
| 76 | + allows the caller to reject charges when the base fee is too high. |
| 77 | + |
| 78 | +### 5. Private Mempool / Censorship |
| 79 | + |
| 80 | +| Threat | Description | Severity | |
| 81 | +|--------|-------------|----------| |
| 82 | +| **Tx Censorship** | A validator censors the subscriber's reveal transaction, letting the commitment expire, then submits their own reveal with a manipulated price. | Medium | |
| 83 | +| **Forced Failure** | Validator delays charge transactions to cause `next_charge_at` violations, then collects late fees or penalties. | Low | |
| 84 | + |
| 85 | +**Mitigations:** |
| 86 | + |
| 87 | +- **Private Mempool Config** (`MevChargeConfig.use_private_mempool`): |
| 88 | + When enabled, the contract emits a |
| 89 | + `MevEventKind::PrivateMempoolSubmitted` event. Off-chain indexers |
| 90 | + forward the event to a private mempool (e.g. via a relayer) to bypass |
| 91 | + public tx visibility. |
| 92 | +- `deadline` on commitments is set by the subscriber. A sufficiently |
| 93 | + long deadline (e.g. several ledger closes) gives the subscriber |
| 94 | + ample time to retry the reveal if censored. |
| 95 | + |
| 96 | +## Architecture Diagram |
| 97 | + |
| 98 | +``` |
| 99 | +Subscriber Contract Storage |
| 100 | + | | | |
| 101 | + |-- commit_charge(hash, fee, dl)-->| | |
| 102 | + | |--- persist ChargeCommitment -->| |
| 103 | + | |--- emit MevEvent::Committed -->| |
| 104 | + | | | |
| 105 | + | ... time passes ... | | |
| 106 | + | | | |
| 107 | + |-- reveal_charge(amount, nonce)->| | |
| 108 | + | |--- load ChargeCommitment ---->| |
| 109 | + | |--- verify sha256 match -------| |
| 110 | + | |--- check base_fee <= max_fee -| |
| 111 | + | |--- token.transfer() --------->| |
| 112 | + | |--- persist GasPriceSnapshot ->| |
| 113 | + | |--- emit MevEvent::Revealed -->| |
| 114 | +``` |
| 115 | + |
| 116 | +## Configuration Reference |
| 117 | + |
| 118 | +| Parameter | Type | Scope | Description | |
| 119 | +|-----------|------|-------|-------------| |
| 120 | +| `use_private_mempool` | `bool` | Per-sub | Emit event for private mempool relay | |
| 121 | +| `max_gas_fee` (config) | `i128` | Per-sub | Base fee ceiling from persistent config | |
| 122 | +| `max_gas_fee` (per-call) | `Option<i128>` | Per-charge | Inline base fee ceiling (overrides config) | |
| 123 | +| `max_gas` | `Option<u64>` | Per-charge | Gas budget ceiling | |
| 124 | +| `commitment_hash` | `Bytes` | Per-charge | SHA-256(amount \|\| nonce \|\| subscriber) | |
| 125 | +| `deadline` | `u64` | Per-charge | Timestamp after which commitment expires | |
| 126 | + |
| 127 | +## Error Codes |
| 128 | + |
| 129 | +| Code | Error | Condition | |
| 130 | +|------|-------|-----------| |
| 131 | +| 22 | `SlippageExceeded` | Base fee exceeds `max_gas_fee` | |
| 132 | +| 23 | `CommitmentExpired` | `now > deadline` on reveal or `deadline < now` on commit | |
| 133 | +| 24 | `CommitmentMismatch` | SHA-256(amount, nonce, subscriber) does not match stored hash | |
| 134 | +| 25 | `MaxGasExceeded` | Actual gas used exceeds `max_gas` | |
| 135 | +| 26 | `PrivateMempoolRequired` | Charge attempted without private mempool when config requires it | |
| 136 | + |
| 137 | +## Monitoring & Alerting |
| 138 | + |
| 139 | +Off-chain indexers should watch for the following events: |
| 140 | + |
| 141 | +| Event Topic | Action | |
| 142 | +|-------------|--------| |
| 143 | +| `mev_event` + `GasPriceAnomaly` | Alert: gas price spike detected for subscriber | |
| 144 | +| `mev_event` + `PrivateMempoolSubmitted` | Verify that the tx was routed through private mempool | |
| 145 | +| `mev_event` + `Expired` | Alert: commitment expired without reveal (possible censorship) | |
| 146 | +| `rate_limit_violation` (on `charge_subscription`) | Check for DoS attempts on the subscriber | |
| 147 | + |
| 148 | +Compare `GasPriceSnapshot.base_fee` across consecutive charges for the |
| 149 | +same subscription. A sudden increase > 2x may indicate a gas price |
| 150 | +attack and should trigger a manual review. |
0 commit comments