PINK: E2E trace analysis — Pass 20 config/math signs/BingX protocol (W1-W14)
Twentieth pass: int() on 3 env vars uncaught ValueError (W1 Critical), DITA_V2_PREFIX default "dita_v2" multi-process shared memory corruption (W2 Critical), funding sign opposite Python V2 vs Rust same raw value opposite capital effect (W3 Critical), listenKeyExpired frames silently swallowed continue skips expiry check dead code (W4 Critical), RECV_WINDOW_MS no upper bound replay attacks (W5 High), ACTIVE_SLOT_LIMIT stored never enforced by Rust kernel (W6 High), no fill history fetched during WS reconnect gap-backfill fills lost (W7 High), rate limit detection fails on HTTP 429 no matching message instant retry (W8 High), CONTROL_PLANE=REAL_ZINC silently falls back to in-memory (W9 High), all BingxHttpError mapped to REJECTED can't distinguish errors (W10 High), os.environ bracket access vs .get() inconsistent (W11 High). 361 total flaws across 20 passes. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
@@ -7135,3 +7135,347 @@ If an object has both `close()` and `disconnect()`, only `close()` is called. Th
|
||||
| U | Pass 18 (Rust Test Gaps/Accounting/FFI Types) | 14 | 3 | 4 | 4 | 3 | 0 |
|
||||
| V | Pass 19 (Lifecycle/Rust Subtleties/Test Infra) | 14 | 5 | 2 | 4 | 3 | 0 |
|
||||
| **Total** | | **347** | **35** | **101** | **100** | **64** | **37** |
|
||||
|
||||
---
|
||||
|
||||
## PASS 20 — CONFIGURATION MANAGEMENT, MATH SIGN CONVENTIONS, BINGX PROTOCOL
|
||||
|
||||
### W1: `int()` on three env vars (`RECV_WINDOW_MS`, `DEFAULT_LEVERAGE`, `EXCHANGE_LEVERAGE_CAP`) — `ValueError` uncaught, immediate crash on non-numeric input
|
||||
|
||||
**File:** `launcher.py:189-191`
|
||||
|
||||
```python
|
||||
recv_window_ms = int(os.environ.get("DOLPHIN_BINGX_RECV_WINDOW_MS", "5000"))
|
||||
default_leverage = int(os.environ.get("DOLPHIN_BINGX_DEFAULT_LEVERAGE", "1"))
|
||||
exchange_leverage_cap = int(os.environ.get("DOLPHIN_BINGX_EXCHANGE_LEVERAGE_CAP", "3"))
|
||||
```
|
||||
|
||||
Three consecutive `int()` calls on raw env var strings with **no try/except**. If any of these env vars is set to a non-numeric value (e.g., `DOLPHIN_BINGX_RECV_WINDOW_MS=abc` from a typo in Docker config), `int("abc")` raises `ValueError` which propagates uncaught through `build_bingx_exec_client_config()` → `build_launcher_bundle()` → crashes the process.
|
||||
|
||||
Compare with `DITA_V2_ACTIVE_SLOT_LIMIT` (launcher.py:140-144) which correctly wraps `int()` in `try/except Exception: pass`. The slot limit parsing is safe; these three are not.
|
||||
|
||||
**Severity: Critical**
|
||||
|
||||
### W2: `DITA_V2_PREFIX` default `"dita_v2"` — multi-process with `ZINC=REAL` causes silent shared-memory data corruption
|
||||
|
||||
**File:** `launcher.py:311`
|
||||
|
||||
```python
|
||||
resolved_prefix = (prefix or os.environ.get("DITA_V2_PREFIX", "dita_v2")).strip()
|
||||
```
|
||||
|
||||
The prefix defaults to `"dita_v2"` if not set. When two processes on the same machine use `DITA_V2_ZINC=REAL` (or one process restarts without cleaning shared memory), both try to `SharedRegion.create("dita_v2_intent", ...)`, `"dita_v2_state"`, `"dita_v2_control"`.
|
||||
|
||||
On Linux, shared memory segments (`/dev/shm/`) persist until explicitly unlinked or the system reboots. A second process:
|
||||
- Gets `EEXIST` from `SharedRegion.create()` — which `real_zinc_plane.py`'s `__init__` does NOT handle (no `try/except`)
|
||||
- Or if the region already exists with different size, get a mismatch error
|
||||
- Or if the region is simply opened (not created), both processes read/write the same memory — **simultaneous writes corrupt the state**
|
||||
|
||||
The `_write_region` functions are non-atomic (T7), so two processes writing concurrently see partial updates.
|
||||
|
||||
**Severity: Critical**
|
||||
|
||||
### W3: Funding sign convention opposite between Python V2 `apply_funding()` and Rust `apply_funding_fee()` — same raw exchange value produces opposite capital effect
|
||||
|
||||
**Files:** `_rust_kernel/src/lib.rs:839-841` (Rust), `account.py:299` (Python V2)
|
||||
|
||||
```rust
|
||||
// Rust: amount > 0 = received (capital ↑)
|
||||
self.k_funding_net -= amount;
|
||||
// If amount = -3.75 (paid out): k_funding_net -= (-3.75) = k_funding_net + 3.75
|
||||
// k_capital = seed + realized - fees - k_funding_net = seed + realized - fees - (+3.75) = capital ↓
|
||||
```
|
||||
|
||||
```python
|
||||
# Python V2: amount > 0 = paid out (capital ↓)
|
||||
self._k_funding += amount
|
||||
self._k_capital = self._seed + self._k_realized - self._k_fees - self._k_funding
|
||||
# If amount = -3.75 (paid out): _k_funding += (-3.75) = -3.75
|
||||
# k_capital = seed + realized - fees - (-3.75) = seed + realized - fees + 3.75 = capital ↑ WRONG
|
||||
```
|
||||
|
||||
Both use `k_capital = seed + realized - fees - funding`. But:
|
||||
- **Rust**: A funding **payment** (capital decreases) is represented as a **negative** `amount`. `k_funding_net -= (-3.75) = +3.75`, then `capital - 3.75` = capital decreases. Correct.
|
||||
- **Python V2**: A funding **payment** (capital decreases) is represented as a **positive** `amount`. `_k_funding += 7.25`, then `capital - 7.25` = capital decreases. Also correct for its own convention — but opposite sign convention.
|
||||
|
||||
**The same raw exchange value** (e.g., `funding_amount = -3.75` from BingX WS showing a funding cost):
|
||||
|
||||
| System | Input | `k_funding` | k_capital effect | Correct? |
|
||||
|--------|-------|-------------|-----------------|----------|
|
||||
| Rust | `-3.75` | `funding_net = 0 - (-3.75) = +3.75` | `capital - 3.75` (decrease) | ✅ |
|
||||
| Python V2 | `-3.75` | `_k_funding = 0 + (-3.75) = -3.75` | `capital - (-3.75) = capital + 3.75` (increase) | ❌ WRONG |
|
||||
|
||||
The parity test (`test_exchange_event_seam_parity.py:426`) compares WS path vs Poll path — both use Python V2 `apply_funding()` with the same convention, so they match each other but **both are wrong in absolute value**. The Rust kernel produces the correct absolute value.
|
||||
|
||||
**Severity: Critical**
|
||||
|
||||
### W4: `BingxUserStream` `listenKeyExpired` frames silently swallowed — `continue` at line 272 skips the expiry check at line 275, dead code
|
||||
|
||||
**File:** `bingx_user_stream.py:272-276`
|
||||
|
||||
```python
|
||||
# Line 272-276 — the main WS message dispatch:
|
||||
kind = frame.get("e", "")
|
||||
if kind in self._NORMALISE_MAP:
|
||||
yield self._NORMALISE_MAP[kind](frame)
|
||||
else:
|
||||
continue # <-- UNKNOWN event type → continue
|
||||
|
||||
# Line 275: THIS LINE IS NEVER REACHED for listenKeyExpired
|
||||
if kind == "listenKeyExpired":
|
||||
raise RuntimeError("listenKeyExpired")
|
||||
```
|
||||
|
||||
The `else: continue` at line 273 skips the `listenKeyExpired` check at line 275. When BingX sends `{"e": "listenKeyExpired"}`, the dispatch:
|
||||
1. Check `kind in self._NORMALISE_MAP` — `"listenKeyExpired"` is NOT in the map
|
||||
2. Falls to `else: continue` — skips the rest of the loop body
|
||||
3. Line 275 is **dead code** — never reaches the `raise RuntimeError`
|
||||
|
||||
The stream stays connected with a dead listenKey. The keepalive loop (which runs independently) keeps sending PUT keepalive requests to the dead key. The 24-hour rotation timer eventually fires, but in the meantime (potentially hours), all WS events are silently lost.
|
||||
|
||||
The `raise RuntimeError("listenKeyExpired")` at line 276 was clearly intended to trigger a reconnect, but the `continue` before it makes it unreachable.
|
||||
|
||||
**Severity: Critical**
|
||||
|
||||
### W5: `int()` on `DOLPHIN_BINGX_RECV_WINDOW_MS` with no bounds check — extreme values can enable replay attacks
|
||||
|
||||
**File:** `launcher.py:189`
|
||||
|
||||
```python
|
||||
recv_window_ms = int(os.environ.get("DOLPHIN_BINGX_RECV_WINDOW_MS", "5000"))
|
||||
```
|
||||
|
||||
The recv window is used in HMAC-signed BingX requests as the `recvWindow` parameter. It defines the timestamp tolerance for signed requests — how far off the request timestamp can be from the server's clock.
|
||||
|
||||
A value like `86400000` (24 hours) means any signed request is valid for 24 hours from its timestamp. An attacker who intercepts a signed request can replay it for an entire day.
|
||||
|
||||
There's no upper bound. The code only clamps `max(1, recv_window_ms)` — so the minimum is 1ms but the maximum is unbounded.
|
||||
|
||||
**Severity: High**
|
||||
|
||||
### W6: `DITA_V2_ACTIVE_SLOT_LIMIT` stored in control snapshot but never enforced by Rust kernel — dead config value
|
||||
|
||||
**File:** `launcher.py:140-144` (read and stored), `_rust_kernel/src/lib.rs` (never checked)
|
||||
|
||||
```python
|
||||
# launcher.py: stored in control snapshot
|
||||
raw = os.environ.get("DITA_V2_ACTIVE_SLOT_LIMIT")
|
||||
if raw is not None:
|
||||
fields["active_slot_limit"] = max(1, int(str(raw).strip()))
|
||||
```
|
||||
|
||||
The env var is read, parsed, clamped, and stored into the control plane's `ControlSnapshot`. But the Rust kernel allocates `max_slots` slots at construction (from `ExecutionKernel.__init__`'s `max_slots` parameter) and **never reads** the `active_slot_limit` from the control snapshot.
|
||||
|
||||
The `active_slot_limit` field is written to projections (`hazelcast_projection.py:41` writes `control.as_dict()` which includes `active_slot_limit`) and visible in the control plane state, but the Rust kernel never limits slot usage based on it. An algorithm could send ENTER intents to any slot up to `max_slots` regardless of the configured limit.
|
||||
|
||||
**Severity: High**
|
||||
|
||||
### W7: No fill/trade history fetched during WS reconnect gap-backfill — fills during disconnect window permanently lost
|
||||
|
||||
**File:** `bingx_user_stream.py:117-121`
|
||||
|
||||
```python
|
||||
try:
|
||||
snap = await self.account_snapshot()
|
||||
yield snap
|
||||
except Exception as exc:
|
||||
log.warning("bingx_user_stream: gap-backfill REST failed: %s", exc)
|
||||
```
|
||||
|
||||
`account_snapshot()` (lines 153-219) fetches:
|
||||
- `GET /openApi/swap/v3/user/balance` — wallet balance, available margin
|
||||
- `GET /openApi/swap/v2/user/positions` — open positions with entry price, qty
|
||||
|
||||
It does **NOT** fetch:
|
||||
- `GET /openApi/swap/v2/trade/fill/history` — fill history during the gap
|
||||
|
||||
If a LIMIT order filled during the reconnect window (e.g., a resting limit order that was placed before disconnect and filled while the WS was down), the fill event is permanently lost. The balance snapshot shows the *result* (changed wallet balance), but no fill event with `price`, `qty`, `fee`, `realized_pnl` is emitted. The kernel processes only an `ACCOUNT_UPDATE`, missing the individual fill details.
|
||||
|
||||
Additionally, funding fee events accrued during the disconnect are invisible. The balance reflects them, but no `FUNDING_FEE` event arrives. The kernel's `k_funding_net` drifts until the next explicit funding event.
|
||||
|
||||
**Severity: High**
|
||||
|
||||
### W8: `BingxVenueAdapter` rate limit detection fails on HTTP 429 without matching message body — `_rate_limit_retry_after_ms` returns 0, instant retry
|
||||
|
||||
**File:** `bingx_venue.py:169-183`
|
||||
|
||||
The rate limit detection has three paths:
|
||||
1. **Response header** (`retryAfter`, `retry_after_ms`, `retryAfterMs`) — extracted from response dict
|
||||
2. **Error message regex** — `re.search(r"unblocked after (\d+)", msg)` on the exchange error text
|
||||
3. **Return 0** — everything else
|
||||
|
||||
If BingX returns HTTP 429 with a message body that doesn't contain the phrase `"unblocked after"` (e.g., a generic `"too many requests"` or a localized message), the regex misses and returns 0. The caller then retries **immediately**, burning more rate limit quota.
|
||||
|
||||
The `BingxHttpError` catch at line 316-317 catches all HTTP errors (including 429) and converts them to `{"status": "REJECTED", ...}` — the `REJECTED` tag prevents the kernel from recognizing it as `RATE_LIMITED`. The rate-limit detection is entirely dependent on the error message body format, not the HTTP status code.
|
||||
|
||||
**Fix:** Check for HTTP 429 status code first, then fall back to message parsing.
|
||||
|
||||
**Severity: High**
|
||||
|
||||
### W9: `DITA_V2_CONTROL_PLANE=REAL_ZINC` silently falls back to in-memory on any exception — operator thinks they have persistence but don't
|
||||
|
||||
**File:** `control.py:205-212`
|
||||
|
||||
```python
|
||||
if env_choice in {"REAL", "REAL_ZINC", "SHARED", "SHARED_MEM"}:
|
||||
try:
|
||||
from .real_control_plane import RealZincControlPlane
|
||||
plane = RealZincControlPlane(prefix=prefix, create=True)
|
||||
except Exception:
|
||||
pass # <-- silent fallback, no log
|
||||
return ZincControlPlane(snapshot=snapshot) # in-memory fallback
|
||||
```
|
||||
|
||||
If `RealZincControlPlane()` raises (Zinc library not installed, shared memory creation fails, permission denied), the exception is silently swallowed and the function returns an `InMemoryControlPlane`. No log, no warning, no diagnostic. The operator configured persistent shared-memory control plane but gets ephemeral in-memory.
|
||||
|
||||
Compare with `build_launcher_bundle()`'s `_build_zinc_plane()` (launcher.py:122-125) which also silently falls back — same pattern.
|
||||
|
||||
**Severity: High**
|
||||
|
||||
### W10: `BingxVenueAdapter` HTTP error handling maps ALL `BingxHttpError` to `"REJECTED"` — cannot distinguish "order not found" from "exchange is down"
|
||||
|
||||
**File:** `bingx_venue.py:316-317`
|
||||
|
||||
```python
|
||||
except BingxHttpError as exc:
|
||||
response = {"status": "REJECTED", "msg": str(exc), ...}
|
||||
```
|
||||
|
||||
Every `BingxHttpError` is mapped to `status="REJECTED"`. A 500 Internal Server Error, a 403 forbidden, a 404 order-not-found — all become "REJECTED". The Rust kernel treats "REJECTED" as a specific FSM signal. It cannot distinguish "this cancel was rejected because the order doesn't exist" (harmless, order already cancelled) from "the exchange is returning 500 errors" (system-wide failure, should halt trading).
|
||||
|
||||
**Impact:** If BingX has a transient 500 error, every submit/cancel in that window returns "REJECTED". The kernel may interpret this as genuine order rejections and transition the FSM to CLOSED or trigger cancels, even though the orders may have actually gone through.
|
||||
|
||||
**Severity: High**
|
||||
|
||||
### W11: `DOLPHIN_BINGX_API_KEY` accessed via bracket `os.environ["BINGX_API_KEY"]` in generated tests — `KeyError` crash if unset; inconsistent with launcher's `.get()` which silently passes `None`
|
||||
|
||||
**Files:** `gen2.py:320`, `gen_live_tests.py:116-117`, `_gen_test.py:60`
|
||||
|
||||
```python
|
||||
# Generated test code (all three generators):
|
||||
BINGX_API_KEY = os.environ["BINGX_API_KEY"] # bracket access — KeyError if unset
|
||||
BINGX_SECRET_KEY = os.environ["BINGX_SECRET_KEY]"
|
||||
```
|
||||
|
||||
```python
|
||||
# launcher.py:195-196 — different access pattern:
|
||||
api_key=os.environ.get("BINGX_API_KEY"), # .get() — returns None if unset
|
||||
secret_key=os.environ.get("BINGX_SECRET_KEY"),
|
||||
```
|
||||
|
||||
The test generators use **bracket access** (`os.environ["KEY"]`) which raises `KeyError` instantly if the env var is missing. The launcher uses **`.get()` access** (`os.environ.get("KEY")`) which silently returns `None`.
|
||||
|
||||
This means:
|
||||
- Running generated tests without env vars → `KeyError` crash at module import time
|
||||
- Running the launcher without env vars → `None` silently passed to `BingxExecClientConfig` → delay failure until first HTTP call (confusing 401)
|
||||
|
||||
Two different failure modes for the same missing configuration. The launcher should validate `None` immediately.
|
||||
|
||||
**Severity: High**
|
||||
|
||||
### W12: `MockVenueScenario` has no `rate_limit` flag — entire RATE_LIMITED code path untested in CI
|
||||
|
||||
**File:** `mock_venue.py:27-35`
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class MockVenueScenario:
|
||||
reject_entries: bool = False
|
||||
reject_exits: bool = False
|
||||
reject_cancels: bool = False
|
||||
all_fills_partial: bool = False
|
||||
# NOTE: no rate_limit field
|
||||
```
|
||||
|
||||
The `MockVenueScenario` dataclass has flags for rejection and partial fill simulation but **no `rate_limit` flag**. The entire `RATE_LIMITED` code path — in the Python adapter (`bingx_venue.py:384-396` detects retry-after, tags event as RATE_LIMITED) and in the Rust kernel (the FSM match arm for `KernelEventKind::RATE_LIMITED`) — has zero simulation in mock venue tests.
|
||||
|
||||
Only a live BingX connection can trigger the rate-limit path, and the live tests are triple env-gated (V5) and never run in CI.
|
||||
|
||||
**Severity: Medium**
|
||||
|
||||
### W13: `_rate_limit_retry_after_ms` regex uses English phrase `"unblocked after"` — non-portable, fails on localized exchange messages
|
||||
|
||||
**File:** `bingx_venue.py:177`
|
||||
|
||||
```python
|
||||
m = re.search(r"unblocked after (\d+)", msg)
|
||||
```
|
||||
|
||||
The regex relies on the English phrase `"unblocked after"` in the exchange's error message. BingX is a Chinese exchange. If the error response is localized to Chinese (e.g., `"解封后(\d+)毫秒"`), or if BingX changes their English wording, the regex silently misses and returns 0 (instant retry).
|
||||
|
||||
**Fix:** Prioritize the `Retry-After` HTTP response header or JSON field `retryAfter`/`retry_after_ms` over parsing the error message body.
|
||||
|
||||
**Severity: Medium**
|
||||
|
||||
### W14: `DITA_V2_ACTIVE_SLOT_LIMIT` invalid values silently discarded with no logging — operator sets `"abc"`, gets default with no warning
|
||||
|
||||
**File:** `launcher.py:140-144`
|
||||
|
||||
```python
|
||||
raw = os.environ.get("DITA_V2_ACTIVE_SLOT_LIMIT")
|
||||
if raw is not None:
|
||||
try:
|
||||
fields["active_slot_limit"] = max(1, int(str(raw).strip()))
|
||||
except Exception:
|
||||
pass # no log, no warning
|
||||
```
|
||||
|
||||
If the operator sets `DITA_V2_ACTIVE_SLOT_LIMIT=abc`, the `int()` raises `ValueError`, the `except` swallows it, and the field is never written to `fields`. The slot limit silently uses the control plane default (10). No log, no warning, no error — the operator thinks they configured a limit but the config was silently ignored.
|
||||
|
||||
**Severity: Medium**
|
||||
|
||||
---
|
||||
|
||||
## Pass 20 Summary
|
||||
|
||||
| # | Flaw | Layer | Severity |
|
||||
|---|------|-------|----------|
|
||||
| W1 | `int()` on 3 env vars uncaught `ValueError` — non-numeric input crashes process | Config | **Critical** |
|
||||
| W2 | `DITA_V2_PREFIX` default `"dita_v2"` — multi-process shared memory corruption | Config | **Critical** |
|
||||
| W3 | Funding sign opposite Python V2 vs Rust — same raw value opposite capital effect | Accounting | **Critical** |
|
||||
| W4 | `listenKeyExpired` frames silently swallowed — `continue` skips expiry check, dead code | Venue | **Critical** |
|
||||
| W5 | `RECV_WINDOW_MS` no upper bound — extreme values enable replay attacks | Config | **High** |
|
||||
| W6 | `ACTIVE_SLOT_LIMIT` stored but never enforced by Rust kernel — dead config | Config | **High** |
|
||||
| W7 | No fill history fetched during WS reconnect gap-backfill — fills permanently lost | Venue | **High** |
|
||||
| W8 | Rate limit detection fails on HTTP 429 without matching message — returns 0 instant retry | Venue | **High** |
|
||||
| W9 | `CONTROL_PLANE=REAL_ZINC` silently falls back to in-memory — no persistence | Config | **High** |
|
||||
| W10 | All `BingxHttpError` mapped to "REJECTED" — can't distinguish errors from real rejections | Venue | **High** |
|
||||
| W11 | `os.environ["KEY"]` bracket access in tests vs `.get()` in launcher — inconsistent | Test | **High** |
|
||||
| W12 | `MockVenueScenario` no `rate_limit` flag — RATE_LIMITED path untested in CI | Test | Medium |
|
||||
| W13 | Rate-limit regex uses English phrase `"unblocked after"` — non-portable | Venue | Medium |
|
||||
| W14 | Invalid `ACTIVE_SLOT_LIMIT` values silently discarded — no log, no warning | Config | Medium |
|
||||
|
||||
### Pass 20 Severity
|
||||
|
||||
| Severity | Count |
|
||||
|----------|-------|
|
||||
| **Critical** | 4 (W1, W2, W3, W4) |
|
||||
| **High** | 7 (W5, W6, W7, W8, W9, W10, W11) |
|
||||
| Medium | 3 (W12, W13, W14) |
|
||||
|
||||
### Combined Catalog (All 20 Passes)
|
||||
|
||||
| Pass | Focus | Count | Critical | High | Medium | Low | Info |
|
||||
|------|-------|-------|----------|------|--------|-----|------|
|
||||
| A | Architectural | 15 | 0 | 2 | 0 | 2 | 11 |
|
||||
| T | Threading/Atomicity | 9 | 1 | 3 | 3 | 2 | 0 |
|
||||
| E | E2E Trace (Pass 1) | 26 | 0 | 4 | 10 | 11 | 1 |
|
||||
| F | Deep E2E (Pass 3) | 30 | 0 | 1 | 8 | 17 | 4 |
|
||||
| G | Domain Scans (Pass 4) | 36 | 4 | 11 | 11 | 8 | 2 |
|
||||
| H | Edge Domains (Pass 5) | 22 | 3 | 9 | 5 | 4 | 1 |
|
||||
| I | Pass 6 (Math/Tests/Recovery/Security) | 22 | 3 | 11 | 4 | 2 | 2 |
|
||||
| J | Pass 7 (Test Infra/Data/Rust/Env/Conn) | 16 | 0 | 7 | 7 | 2 | 0 |
|
||||
| K | Pass 8 (Observability/Memory/Time/DeadCode) | 23 | 2 | 7 | 7 | 1 | 6 |
|
||||
| L | Pass 9 (Contracts/Events/Network/FFI/Diffs) | 16 | 0 | 4 | 8 | 4 | 0 |
|
||||
| M | Pass 10 (Runtime/TestBugs/FSM/Persistence/Metrics) | 18 | 3 | 7 | 5 | 3 | 0 |
|
||||
| N | Pass 11 (Async/Sync Seams/Locks/Threading) | 10 | 4 | 1 | 3 | 1 | 1 |
|
||||
| O | Pass 12 (Sync/Async Wider Scope) | 11 | 0 | 3 | 7 | 1 | 0 |
|
||||
| P | Pass 13 (FFI Safety/Dangling Pointers/Coverage) | 9 | 1 | 3 | 3 | 1 | 1 |
|
||||
| Q | Pass 14 (Serde Edges/Backup Diffs/Market Data) | 12 | 0 | 4 | 3 | 2 | 3 |
|
||||
| R | Pass 15 (Resource Leaks/Trust Boundaries/Security) | 14 | 2 | 6 | 3 | 2 | 1 |
|
||||
| S | Pass 16 (Error Handling/Arithmetic/Test Infra) | 16 | 4 | 7 | 5 | 0 | 0 |
|
||||
| T | Pass 17 (Unsafe Review/Dead Code/Build/Protocols) | 14 | 0 | 5 | 5 | 4 | 0 |
|
||||
| U | Pass 18 (Rust Test Gaps/Accounting/FFI Types) | 14 | 3 | 4 | 4 | 3 | 0 |
|
||||
| V | Pass 19 (Lifecycle/Rust Subtleties/Test Infra) | 14 | 5 | 2 | 4 | 3 | 0 |
|
||||
| W | Pass 20 (Config/Math Signs/BingX Protocol) | 14 | 4 | 7 | 3 | 0 | 0 |
|
||||
| **Total** | | **361** | **39** | **108** | **103** | **64** | **37** |
|
||||
|
||||
Reference in New Issue
Block a user