bug-bounty-intelligence-mcp 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/BENCHMARK.md CHANGED
@@ -1,174 +1,174 @@
1
- # Benchmark: Bug Bounty Intelligence vs Slither
2
-
3
- **Target protocol:** 3FLabs/grunt — leveraged strategies for on-chain funds
4
- **Protocol type:** ERC-4626/ERC-7540 vaults + Morpho lending + flash loan requests
5
- **Scope:** `src/` (218 Solidity files)
6
- **Slither version:** 0.10.x
7
- **Date:** 2026-07-20
8
-
9
- ---
10
-
11
- ## TL;DR
12
-
13
- | Metric | Slither | Bug Bounty Intelligence |
14
- |--------|---------|------------------------|
15
- | "High" findings reported | 27 | 0 |
16
- | True positives (real bugs) | 0 | 0 |
17
- | False positives | 27 (100%) | 0 |
18
- | Out-of-scope library noise | 24 (89%) | 0 |
19
- | Signals requiring human review | n/a | 17 |
20
- | Reading time to triage all "Highs" | ~4 hours | ~15 min |
21
-
22
- **Slither false positive rate on this codebase: 100%.**
23
-
24
- ---
25
-
26
- ## Test setup
27
-
28
- We ran Slither on the full repo (including `lib/`), and Bug Bounty Intelligence on `src/` only. The scope for any real audit is `src/` — dependencies in `lib/` are battle-tested third-party code that has its own audit history.
29
-
30
- ---
31
-
32
- ## Slither's 27 "High" findings — breakdown
33
-
34
- ### Group A: 24 findings in `lib/solady/` (out of scope)
35
-
36
- Slither flagged 24 High-severity issues, all in the `lib/solady/` dependency:
37
-
38
- - 5x `incorrect-exp` — Solady uses XOR (`^`) for bitwise operations, not exponentiation. Slither confuses the two.
39
- - 19x `incorrect-shift` — Solady uses `<<` / `>>` inside assembly blocks. Slither's detector fires on these but they are intentional and correct.
40
-
41
- **Why these are not bugs:**
42
-
43
- Solady is one of the most audited Solidity libraries in existence, used by hundreds of protocols. Its math implementations deliberately use low-level bit operations for gas efficiency. Slither's `incorrect-exp` and `incorrect-shift` detectors are known to produce false positives on Solady. Every human auditor knows to ignore these. Slither does not.
44
-
45
- **Example:**
46
-
47
- ```
48
- incorrect-shift | FixedPointMathLib.lnWad(int256) (lib/solady/src/utils/FixedPointMathLib.sol#269-341)
49
- contains an incorrect shift operation: ...
50
- ```
51
-
52
- This is in `lib/solady/`, a third-party library. It is out of scope for any 3FLabs audit.
53
-
54
- **Update:** while triaging these, we noticed a related pattern on the same `lib/solady` code:
55
- `divide-before-multiply` fired 38 times on `FixedPointMathLib`'s `fullMulDivUnchecked`, `cbrt`,
56
- `invMod`, `rpow`, and `cbrtWad`. We initially described all 38 as the same false-positive class
57
- (divided variable fully reassigned before the multiply) and reported it upstream —
58
- [crytic/slither#3039](https://github.com/crytic/slither/issues/3039). That was an overclaim on our
59
- part. A maintainer merged a fix for the specific reassignment pattern we described
60
- ([crytic/slither#3040](https://github.com/crytic/slither/pull/3040), not yet in a PyPI release as
61
- of 2026-08-01), and re-testing against the merged code ourselves shows it correctly resolves 11 of
62
- the 38 (down to 27 remaining on this file). The other 27 keep the divided variable in the same
63
- expression rather than through a clean reassignment, which is a different pattern this fix
64
- correctly leaves untouched — we haven't yet re-analyzed whether those are genuine false positives
65
- too or something else. Filed as a lesson: verify a fix against the actual code before describing
66
- it as resolving the full scope of what you reported.
67
-
68
- ### Group B: 3 findings in `src/` — all false positives
69
-
70
- Slither flagged 3 uses of `safeTransferFrom(offer.maker, ...)` as "arbitrary from in transferFrom":
71
-
72
- ```
73
- arbitrary-send-erc20 | Request.consume() uses arbitrary from in transferFrom:
74
- _asset().safeTransferFrom(offer.maker, address(this), ptAmount)
75
- ```
76
-
77
- **Why this is not a bug:**
78
-
79
- `offer.maker` is not arbitrary. The offer is validated via EIP-712 signature before the transfer (`_validateOffer(offer, signature)` is called on line 421). The maker explicitly signed the offer, authorizing exactly this transfer of exactly this amount. Additionally, `consume()` is gated by `onlyOwnerOrRoles(_ROLE_CONSUMER)` — only trusted consumers can call it.
80
-
81
- This is a correct and standard EIP-712 off-chain order pattern. Slither has no understanding of EIP-712 and cannot reason about signature-based authorization.
82
-
83
- ---
84
-
85
- ## What Bug Bounty Intelligence flags instead
86
-
87
- After filtering to `src/` and applying Al-Mizaan validation gates, we identified **17 signals requiring human review** — none rising to a confirmed finding:
88
-
89
- | Category | Count | Description |
90
- |----------|-------|-------------|
91
- | `unsafe_keyword` | 15 | Explicit `forge-lint: disable-next-line(unsafe-typecast)` annotations — developer acknowledged unsafe casts. Require review for truncation risk. |
92
- | `delegatecall` | 2 | `MorphoFlashLoanRequest.execute()` delegatecalls to whitelisted scripts. By design — whitelist check present. |
93
-
94
- **What we do not flag:**
95
- - Library code in `lib/` (out of scope)
96
- - EIP-712 signed transfers (intentional pattern, not a vulnerability)
97
- - Standard Solady bit operations (well-audited library behavior)
98
-
99
- ---
100
-
101
- ## Al-Mizaan filtering — why the 17 signals do not become findings
102
-
103
- The 15 `unsafe_keyword` signals (explicit forge-lint disables) were put through Al-Mizaan's reachability chain:
104
-
105
- 1. **Code** — read the actual casts in `LibTokenController.sol`, `TokenController.sol`, and `PositionManagerLP.sol`
106
- 2. **Reachability** — are the truncated values reachable by an untrusted caller?
107
- 3. **Threat model** — who triggers these paths? In all cases, the entry points are role-gated (owner, ROLE_CONSUMER, ROLE_MANAGER)
108
- 4. **Invariant** — does the truncation break any accounting invariant?
109
-
110
- Verdict: The typecasts are `uint256 → uint128` in share accounting. The values come from internal calculations that are capped by deposit limits. Overflow requires either extremely large TVL or a role-gated actor. **Trusted actor as sole trigger → ELIMINATE.**
111
-
112
- The 2 `delegatecall` signals: `MorphoFlashLoanRequest` maintains an explicit `isScriptWhitelisted` mapping. Only the owner can whitelist. **Trusted actor (owner) as sole attacker → ELIMINATE.**
113
-
114
- **Result: 0 confirmed findings.** This matches the expected difficulty level of a mature protocol with prior audits.
115
-
116
- ---
117
-
118
- ## Why Slither's false positive rate matters
119
-
120
- A 100% false positive rate on High findings is not an edge case. It happens because:
121
-
122
- 1. **No scope awareness** — Slither scans everything, including dependencies. A real audit scopes to `src/` only.
123
- 2. **No protocol context** — Slither cannot distinguish between an arbitrary `transferFrom` and an EIP-712-authorized one.
124
- 3. **No library knowledge** — Solady's assembly patterns trigger false positives on multiple detectors.
125
-
126
- For a developer running Slither for the first time, triaging 27 "High" findings (all false positives) takes hours. For a developer using Bug Bounty Intelligence, the 17 signals that remain after scope filtering take 15 minutes to review.
127
-
128
- ---
129
-
130
- ## When Slither IS useful
131
-
132
- Slither is best used as a first-pass tool before human review, with explicit exclusions:
133
-
134
- ```bash
135
- slither src/ --exclude incorrect-exp,incorrect-shift --filter-paths "lib/"
136
- ```
137
-
138
- After filtering, Slither's Medium and Low findings become more useful starting points.
139
-
140
- Bug Bounty Intelligence is designed to do this filtering for you, and adds protocol-context reasoning that static analysis cannot.
141
-
142
- ---
143
-
144
- ## Reproduce this benchmark
145
-
146
- The 3FLabs/grunt codebase is public at [github.com/3FLabs/grunt](https://github.com/3FLabs/grunt).
147
-
148
- To reproduce Slither's output:
149
- ```bash
150
- git clone https://github.com/3FLabs/grunt
151
- cd grunt
152
- pip install slither-analyzer
153
- slither . --json slither-output.json
154
- ```
155
-
156
- To use Bug Bounty Intelligence:
157
- ```bash
158
- npx -y bug-bounty-intelligence-mcp@latest
159
- ```
160
-
161
- Then in your MCP client:
162
- ```
163
- scan_contract({ repo_url: "https://github.com/3FLabs/grunt", protocol_type: "LENDING" })
164
- ```
165
-
166
- ---
167
-
168
- ## About Bug Bounty Intelligence
169
-
170
- Pattern-aware from a corpus of 27,681 submitted Sherlock/Code4rena findings across 105 contests; acceptance-rate numbers (used by `list_vulnerability_patterns`) are limited to the 1,032 findings across 10 contests that could be exact-reconciled against contest outcomes (461 accepted) — see METHODOLOGY.md. Uses the Al-Mizaan v3 framework — a 7-gate validation chain (Code, Reachability, Threat-Model, Invariant, Protocol-Intent, Impact, Proof) — to filter signals into confirmed findings.
171
-
172
- **Cost:** $5 USDC on Base (eip155:8453) via x402.
173
- **Install:** `npx -y bug-bounty-intelligence-mcp@latest`
174
- **npm:** [npmjs.com/package/bug-bounty-intelligence-mcp](https://www.npmjs.com/package/bug-bounty-intelligence-mcp)
1
+ # Benchmark: Bug Bounty Intelligence vs Slither
2
+
3
+ **Target protocol:** 3FLabs/grunt — leveraged strategies for on-chain funds
4
+ **Protocol type:** ERC-4626/ERC-7540 vaults + Morpho lending + flash loan requests
5
+ **Scope:** `src/` (218 Solidity files)
6
+ **Slither version:** 0.10.x
7
+ **Date:** 2026-07-20
8
+
9
+ ---
10
+
11
+ ## TL;DR
12
+
13
+ | Metric | Slither | Bug Bounty Intelligence |
14
+ |--------|---------|------------------------|
15
+ | "High" findings reported | 27 | 0 |
16
+ | True positives (real bugs) | 0 | 0 |
17
+ | False positives | 27 (100%) | 0 |
18
+ | Out-of-scope library noise | 24 (89%) | 0 |
19
+ | Signals requiring human review | n/a | 17 |
20
+ | Reading time to triage all "Highs" | ~4 hours | ~15 min |
21
+
22
+ **Slither false positive rate on this codebase: 100%.**
23
+
24
+ ---
25
+
26
+ ## Test setup
27
+
28
+ We ran Slither on the full repo (including `lib/`), and Bug Bounty Intelligence on `src/` only. The scope for any real audit is `src/` — dependencies in `lib/` are battle-tested third-party code that has its own audit history.
29
+
30
+ ---
31
+
32
+ ## Slither's 27 "High" findings — breakdown
33
+
34
+ ### Group A: 24 findings in `lib/solady/` (out of scope)
35
+
36
+ Slither flagged 24 High-severity issues, all in the `lib/solady/` dependency:
37
+
38
+ - 5x `incorrect-exp` — Solady uses XOR (`^`) for bitwise operations, not exponentiation. Slither confuses the two.
39
+ - 19x `incorrect-shift` — Solady uses `<<` / `>>` inside assembly blocks. Slither's detector fires on these but they are intentional and correct.
40
+
41
+ **Why these are not bugs:**
42
+
43
+ Solady is one of the most audited Solidity libraries in existence, used by hundreds of protocols. Its math implementations deliberately use low-level bit operations for gas efficiency. Slither's `incorrect-exp` and `incorrect-shift` detectors are known to produce false positives on Solady. Every human auditor knows to ignore these. Slither does not.
44
+
45
+ **Example:**
46
+
47
+ ```
48
+ incorrect-shift | FixedPointMathLib.lnWad(int256) (lib/solady/src/utils/FixedPointMathLib.sol#269-341)
49
+ contains an incorrect shift operation: ...
50
+ ```
51
+
52
+ This is in `lib/solady/`, a third-party library. It is out of scope for any 3FLabs audit.
53
+
54
+ **Update:** while triaging these, we noticed a related pattern on the same `lib/solady` code:
55
+ `divide-before-multiply` fired 38 times on `FixedPointMathLib`'s `fullMulDivUnchecked`, `cbrt`,
56
+ `invMod`, `rpow`, and `cbrtWad`. We initially described all 38 as the same false-positive class
57
+ (divided variable fully reassigned before the multiply) and reported it upstream —
58
+ [crytic/slither#3039](https://github.com/crytic/slither/issues/3039). That was an overclaim on our
59
+ part. A maintainer merged a fix for the specific reassignment pattern we described
60
+ ([crytic/slither#3040](https://github.com/crytic/slither/pull/3040), not yet in a PyPI release as
61
+ of 2026-08-01), and re-testing against the merged code ourselves shows it correctly resolves 11 of
62
+ the 38 (down to 27 remaining on this file). The other 27 keep the divided variable in the same
63
+ expression rather than through a clean reassignment, which is a different pattern this fix
64
+ correctly leaves untouched — we haven't yet re-analyzed whether those are genuine false positives
65
+ too or something else. Filed as a lesson: verify a fix against the actual code before describing
66
+ it as resolving the full scope of what you reported.
67
+
68
+ ### Group B: 3 findings in `src/` — all false positives
69
+
70
+ Slither flagged 3 uses of `safeTransferFrom(offer.maker, ...)` as "arbitrary from in transferFrom":
71
+
72
+ ```
73
+ arbitrary-send-erc20 | Request.consume() uses arbitrary from in transferFrom:
74
+ _asset().safeTransferFrom(offer.maker, address(this), ptAmount)
75
+ ```
76
+
77
+ **Why this is not a bug:**
78
+
79
+ `offer.maker` is not arbitrary. The offer is validated via EIP-712 signature before the transfer (`_validateOffer(offer, signature)` is called on line 421). The maker explicitly signed the offer, authorizing exactly this transfer of exactly this amount. Additionally, `consume()` is gated by `onlyOwnerOrRoles(_ROLE_CONSUMER)` — only trusted consumers can call it.
80
+
81
+ This is a correct and standard EIP-712 off-chain order pattern. Slither has no understanding of EIP-712 and cannot reason about signature-based authorization.
82
+
83
+ ---
84
+
85
+ ## What Bug Bounty Intelligence flags instead
86
+
87
+ After filtering to `src/` and applying Al-Mizaan validation gates, we identified **17 signals requiring human review** — none rising to a confirmed finding:
88
+
89
+ | Category | Count | Description |
90
+ |----------|-------|-------------|
91
+ | `unsafe_keyword` | 15 | Explicit `forge-lint: disable-next-line(unsafe-typecast)` annotations — developer acknowledged unsafe casts. Require review for truncation risk. |
92
+ | `delegatecall` | 2 | `MorphoFlashLoanRequest.execute()` delegatecalls to whitelisted scripts. By design — whitelist check present. |
93
+
94
+ **What we do not flag:**
95
+ - Library code in `lib/` (out of scope)
96
+ - EIP-712 signed transfers (intentional pattern, not a vulnerability)
97
+ - Standard Solady bit operations (well-audited library behavior)
98
+
99
+ ---
100
+
101
+ ## Al-Mizaan filtering — why the 17 signals do not become findings
102
+
103
+ The 15 `unsafe_keyword` signals (explicit forge-lint disables) were put through Al-Mizaan's reachability chain:
104
+
105
+ 1. **Code** — read the actual casts in `LibTokenController.sol`, `TokenController.sol`, and `PositionManagerLP.sol`
106
+ 2. **Reachability** — are the truncated values reachable by an untrusted caller?
107
+ 3. **Threat model** — who triggers these paths? In all cases, the entry points are role-gated (owner, ROLE_CONSUMER, ROLE_MANAGER)
108
+ 4. **Invariant** — does the truncation break any accounting invariant?
109
+
110
+ Verdict: The typecasts are `uint256 → uint128` in share accounting. The values come from internal calculations that are capped by deposit limits. Overflow requires either extremely large TVL or a role-gated actor. **Trusted actor as sole trigger → ELIMINATE.**
111
+
112
+ The 2 `delegatecall` signals: `MorphoFlashLoanRequest` maintains an explicit `isScriptWhitelisted` mapping. Only the owner can whitelist. **Trusted actor (owner) as sole attacker → ELIMINATE.**
113
+
114
+ **Result: 0 confirmed findings.** This matches the expected difficulty level of a mature protocol with prior audits.
115
+
116
+ ---
117
+
118
+ ## Why Slither's false positive rate matters
119
+
120
+ A 100% false positive rate on High findings is not an edge case. It happens because:
121
+
122
+ 1. **No scope awareness** — Slither scans everything, including dependencies. A real audit scopes to `src/` only.
123
+ 2. **No protocol context** — Slither cannot distinguish between an arbitrary `transferFrom` and an EIP-712-authorized one.
124
+ 3. **No library knowledge** — Solady's assembly patterns trigger false positives on multiple detectors.
125
+
126
+ For a developer running Slither for the first time, triaging 27 "High" findings (all false positives) takes hours. For a developer using Bug Bounty Intelligence, the 17 signals that remain after scope filtering take 15 minutes to review.
127
+
128
+ ---
129
+
130
+ ## When Slither IS useful
131
+
132
+ Slither is best used as a first-pass tool before human review, with explicit exclusions:
133
+
134
+ ```bash
135
+ slither src/ --exclude incorrect-exp,incorrect-shift --filter-paths "lib/"
136
+ ```
137
+
138
+ After filtering, Slither's Medium and Low findings become more useful starting points.
139
+
140
+ Bug Bounty Intelligence is designed to do this filtering for you, and adds protocol-context reasoning that static analysis cannot.
141
+
142
+ ---
143
+
144
+ ## Reproduce this benchmark
145
+
146
+ The 3FLabs/grunt codebase is public at [github.com/3FLabs/grunt](https://github.com/3FLabs/grunt).
147
+
148
+ To reproduce Slither's output:
149
+ ```bash
150
+ git clone https://github.com/3FLabs/grunt
151
+ cd grunt
152
+ pip install slither-analyzer
153
+ slither . --json slither-output.json
154
+ ```
155
+
156
+ To use Bug Bounty Intelligence:
157
+ ```bash
158
+ npx -y bug-bounty-intelligence-mcp@latest
159
+ ```
160
+
161
+ Then in your MCP client:
162
+ ```
163
+ scan_contract({ repo_url: "https://github.com/3FLabs/grunt", protocol_type: "LENDING" })
164
+ ```
165
+
166
+ ---
167
+
168
+ ## About Bug Bounty Intelligence
169
+
170
+ Pattern-aware from a corpus of 56,657 submitted Sherlock findings across 212 measured contests, acceptance-rate numbers (used by `list_vulnerability_patterns`) are limited to the 7,388 findings across 35 contests that could be exact-reconciled against contest outcomes (3,694 accepted), see METHODOLOGY.md. Uses the Al-Mizaan v3 framework, a 7-gate validation chain (Code, Reachability, Threat-Model, Invariant, Protocol-Intent, Impact, Proof), to filter signals into confirmed findings.
171
+
172
+ **Cost:** $5 USDC on Base (eip155:8453) via x402.
173
+ **Install:** `npx -y bug-bounty-intelligence-mcp@latest`
174
+ **npm:** [npmjs.com/package/bug-bounty-intelligence-mcp](https://www.npmjs.com/package/bug-bounty-intelligence-mcp)
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Abdellah Ouadoudi
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdellah Ouadoudi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/METHODOLOGY.md CHANGED
@@ -1,94 +1,109 @@
1
- # Methodology: acceptance-rate data behind `list_vulnerability_patterns`
2
-
3
- **Last updated: 2026-07-21.**
4
-
5
- ## What this covers
6
-
7
- `list_vulnerability_patterns` returns, per vulnerability class, what fraction of *submitted*
8
- audit-competition findings were judged valid. This document explains exactly how those numbers
9
- are computed, what they don't claim, and how to reproduce them from public data.
10
-
11
- ## Data source
12
-
13
- Sherlock runs public GitHub repos for judging each audit contest
14
- (`sherlock-audit/<contest>-judging`), where security researchers ("Watsons") open one GitHub
15
- issue per submitted finding. 105 of these judging repos were crawled (27,681 raw submitted
16
- findings total, 2024-02 through 2025-07).
17
-
18
- ## The problem: "was this accepted?" is not reliably answerable from GitHub alone
19
-
20
- An early version of this pipeline inferred acceptance from GitHub issue state
21
- (`OPEN` = accepted, `CLOSED` = rejected). That heuristic turned out to be wrong, in both
22
- directions, depending on the contest — sometimes undercounting acceptance by an order of
23
- magnitude, sometimes overcounting it. GitHub issue labels (`Reward`, `Non-Reward`, `Invalid`,
24
- etc.) turned out to be applied inconsistently across contests too — some contests label every
25
- issue, some label almost none.
26
-
27
- ## The fix: use Sherlock's own result structure, and only trust it when it reconciles
28
-
29
- Sherlock's judging repos also contain, at the repo root, a folder per accepted finding
30
- (`001-M/`, `002-H/`, etc. — number + severity), each containing one file per GitHub issue number
31
- that was judged to be that finding (the primary write-up plus any duplicates), and a separate
32
- `invalid/` folder for rejected issue numbers. This is Sherlock's own authoritative accounting,
33
- not an inference.
34
-
35
- We only trust this for a given contest if it **reconciles exactly**:
36
- `accepted_issue_numbers + invalid_issue_numbers === total_submitted_issues`.
37
-
38
- If a contest's folder structure doesn't exist, or doesn't reconcile exactly, that contest is
39
- **excluded entirely** — not estimated, not approximated. Of 105 crawled contests:
40
-
41
- - **10 reconcile exactly** and are used for `list_vulnerability_patterns`: 1,032 findings, 461
42
- accepted.
43
- - **95 do not** — either no accepted/invalid folder structure exists in that judging repo (a
44
- likely explanation: some contests use a different Sherlock judging workflow that doesn't
45
- generate this structure), or the counts don't sum to the known total (a likely explanation for
46
- many of these: the original crawl capped issue retrieval at 500 per contest, so large contests'
47
- true totals exceed what was captured — a fixable crawler limitation, not a Sherlock data
48
- problem, but not yet re-verified).
49
-
50
- ## What the resulting numbers mean
51
-
52
- For each pattern (keyword-matched against finding titles/bodies — e.g. "reentrancy",
53
- "oracle manipulation"), the acceptance rate is: of all submissions whose issue number appears in
54
- *any* accepted-finding folder (including duplicates of the same underlying bug), what fraction
55
- of total tagged submissions that is. This measures **submission-level reliability** — "if
56
- someone writes up and submits a claim of this bug class, how often does it hold up" — not the
57
- number of distinct vulnerabilities found.
58
-
59
- ## What this does not claim
60
-
61
- - Not a claim about real-world exploit frequency — this is pre-deployment audit-competition
62
- data, not realized incidents.
63
- - Not a claim about detection coverage — it describes submitted reports, not silent misses.
64
- - The keyword-based pattern tagging is a simple substring matcher (see `detectPatterns()` in the
65
- crawler scripts), not a semantic classifier — a manual audit found roughly 65% tagging
66
- precision on a sample. Pattern labels are directional, not exact classifications.
67
- - The 10-contest sample is small. Some pattern rows have thin n (e.g. `liquidation`, n=14) —
68
- treat low-n rows as lower-confidence than high-n rows (e.g. `trusted-actor`, n=311).
69
-
70
- ## Reproducing this
71
-
72
- Everything below uses only public data — no private API, no authentication required beyond
73
- default `gh` CLI rate limits.
74
-
75
- ```bash
76
- # 1. List Sherlock judging repos
77
- gh repo list sherlock-audit --limit 1000 --json name | jq -r '.[] | select(.name | endswith("-judging")) | .name'
78
-
79
- # 2. For a given contest, pull its full file tree and look for the NNN-severity / invalid folders
80
- gh api repos/sherlock-audit/<contest>-judging/git/trees/main?recursive=1 \
81
- | jq '[.tree[].path | select(test("^[0-9]{3}-[HM]/|^invalid/"))]'
82
-
83
- # 3. Cross-check reconciliation against the contest's total issue count
84
- gh api "repos/sherlock-audit/<contest>-judging/issues?state=all&per_page=100" | jq 'length'
85
- ```
86
-
87
- A finding's issue number appearing under any `NNN-[HM]/` path = accepted. Under `invalid/` =
88
- rejected. If `accepted + invalid !== total`, do not use that contest's data.
89
-
90
- ## Why we publish this instead of just the bigger, older number
91
-
92
- An earlier draft of this tool advertised acceptance percentages computed from the full,
93
- unverified 27,681-finding corpus. Independent review caught that those specific percentages
94
- were unreliable. We would rather ship a smaller number we can prove than a bigger one we can't.
1
+ # Methodology: acceptance-rate data behind `list_vulnerability_patterns`
2
+
3
+ **Last updated: 2026-11-12.**
4
+
5
+ ## What this covers
6
+
7
+ `list_vulnerability_patterns` returns, per vulnerability class, what fraction of *submitted*
8
+ audit-competition findings were judged valid. This document explains exactly how those numbers
9
+ are computed, what they don't claim, and how to reproduce them from public data.
10
+
11
+ ## Data source
12
+
13
+ Sherlock runs public GitHub repos for judging each audit contest
14
+ (`sherlock-audit/<contest>-judging`), where security researchers ("Watsons") open one GitHub
15
+ issue per submitted finding. 218 of these judging repos were crawled; a real issue count could be
16
+ confirmed for 212 of them, 56,657 raw submitted findings total (2022-09 through 2025-02).
17
+
18
+ ## The problem: "was this accepted?" is not reliably answerable from GitHub alone
19
+
20
+ An early version of this pipeline inferred acceptance from GitHub issue state
21
+ (`OPEN` = accepted, `CLOSED` = rejected). That heuristic turned out to be wrong, in both
22
+ directions, depending on the contest — sometimes undercounting acceptance by an order of
23
+ magnitude, sometimes overcounting it. GitHub issue labels (`Reward`, `Non-Reward`, `Invalid`,
24
+ etc.) turned out to be applied inconsistently across contests too — some contests label every
25
+ issue, some label almost none.
26
+
27
+ ## The fix: use Sherlock's own result structure, and only trust it when it reconciles
28
+
29
+ Sherlock's judging repos also contain, at the repo root, a folder per accepted finding
30
+ (`001-M/`, `002-H/`, etc. — number + severity), each containing one file per GitHub issue number
31
+ that was judged to be that finding (the primary write-up plus any duplicates), and a separate
32
+ `invalid/` folder for rejected issue numbers. This is Sherlock's own authoritative accounting,
33
+ not an inference.
34
+
35
+ We only trust this for a given contest if it **reconciles exactly**:
36
+ `accepted_issue_numbers + invalid_issue_numbers === total_submitted_issues`.
37
+
38
+ If a contest's folder structure doesn't exist, or doesn't reconcile exactly, that contest is
39
+ **excluded entirely** — not estimated, not approximated. Of 218 crawled contests:
40
+
41
+ - **35 reconcile exactly** and are used for `list_vulnerability_patterns`: 7,388 findings, 3,694
42
+ accepted.
43
+ - **183 do not** — either no accepted/invalid folder structure exists in that judging repo (a
44
+ likely explanation: some contests use a different Sherlock judging workflow that doesn't
45
+ generate this structure), or the counts don't sum to the real total.
46
+
47
+ **2026-11-12 re-verification.** The original 2026-07-21 run (105 contests, 1,032 reconciled
48
+ findings) turned out to rest on a locally cached issue count that was itself wrong for most
49
+ contests, not only for the handful capped at the documented 500-issue crawl limit. Spot check: the
50
+ cache said `2022-09-knox-judging` had 12 submissions; the real count is 153.
51
+ `2024-02-optimism-2024-judging` showed 4; the real count is 251. The fix was to stop trusting any
52
+ locally cached total and re-derive it fresh from GitHub's search API for every contest, which is
53
+ also how the reconciliation check itself works (see Reproducing this, below). Re-running the exact
54
+ same reconciliation rule this way, with no local cache in the loop, is what produced the 35/7,388/
55
+ 3,694 figures above.
56
+
57
+ ## What the resulting numbers mean
58
+
59
+ For each pattern (keyword-matched against finding titles/bodies — e.g. "reentrancy",
60
+ "oracle manipulation"), the acceptance rate is: of all submissions whose issue number appears in
61
+ *any* accepted-finding folder (including duplicates of the same underlying bug), what fraction
62
+ of total tagged submissions that is. This measures **submission-level reliability** — "if
63
+ someone writes up and submits a claim of this bug class, how often does it hold up" — not the
64
+ number of distinct vulnerabilities found.
65
+
66
+ ## What this does not claim
67
+
68
+ - Not a claim about real-world exploit frequency — this is pre-deployment audit-competition
69
+ data, not realized incidents.
70
+ - Not a claim about detection coverage — it describes submitted reports, not silent misses.
71
+ - The keyword-based pattern tagging is a simple substring matcher (see `detectPatterns()` in the
72
+ crawler scripts), not a semantic classifier — a manual audit found roughly 65% tagging
73
+ precision on a sample. Pattern labels are directional, not exact classifications.
74
+ - The 35-contest sample is still small relative to the full Sherlock history. Some pattern rows
75
+ have thin n (e.g. `idor`, n=18) — treat low-n rows as lower-confidence than high-n rows (e.g.
76
+ `trusted-actor`, n=1,814).
77
+
78
+ ## Reproducing this
79
+
80
+ Everything below uses only public data — no private API, no authentication required beyond
81
+ default `gh` CLI rate limits.
82
+
83
+ ```bash
84
+ # 1. List Sherlock judging repos
85
+ gh repo list sherlock-audit --limit 1000 --json name | jq -r '.[] | select(.name | endswith("-judging")) | .name'
86
+
87
+ # 2. For a given contest, pull its full file tree and look for the NNN-severity / invalid folders
88
+ gh api repos/sherlock-audit/<contest>-judging/git/trees/main?recursive=1 \
89
+ | jq '[.tree[].path | select(test("^[0-9]{3}-[HM]/|^invalid/"))]'
90
+
91
+ # 3. Cross-check reconciliation against the contest's REAL total issue count. Do not paginate
92
+ # an issues?state=all list and take its length: that silently undercounts the moment a
93
+ # contest has more issues than one page covers, which is most of them. The search API's
94
+ # own total_count does not have this problem.
95
+ gh api "search/issues?q=repo:sherlock-audit/<contest>-judging+type:issue" --jq .total_count
96
+ ```
97
+
98
+ A finding's issue number appearing under any `NNN-[HM]/` path = accepted (strip any leading
99
+ zeros before comparing to the issue number from the GitHub API, the folder names are zero-padded
100
+ and the API numbers are not). Under `invalid/` = rejected. If `accepted + invalid !== total`, do
101
+ not use that contest's data.
102
+
103
+ ## Why we publish this instead of just the bigger, older number
104
+
105
+ An earlier draft of this tool advertised acceptance percentages computed from the full,
106
+ unverified raw corpus. Independent review caught that those specific percentages were
107
+ unreliable. We would rather ship a smaller number we can prove than a bigger one we can't. That
108
+ is still true today: the raw corpus is 56,657 findings, the reconciled, trustworthy number
109
+ published here is 7,388.