Threshold tBTC v2 (BOB) — SPECTRE Scan Foundation (2026-05-19)
Target: threshold-network/tbtc-v2 — cross-chain/bob/contracts
Bounty: Immunefi — Threshold
Scope: Solidity (.sol) — EVM L2 (BOB) token-pool + bridge integration
TL;DR
Before this commit, pinpoint spectre scan against this repo returned Vertices: 0, Edges: 0 — SPECTRE had no Solidity language extractor. The Solana-focused rule packs (pinpoint-rules-solana, pinpoint-rules-security, etc.) ran against an empty graph and produced zero findings on every Solidity codebase.
This commit ships pinpoint-lang-solidity, the first non-Solana language extractor in SPECTRE, and a new lang-solidity / evm feature surface in the CLI. The post-build scan on the tBTC v2 BOB contracts populates 182 vertices / 414 edges across 173 .sol files — the graph foundation that future EVM rule packs will pattern-match against.
| Stage | Result |
|---|---|
| Pre-fix scan | 0 vertices, 0 edges, "no languages detected" |
| Post-fix scan | 182 vertices, 414 edges, 173 Solidity files, 23+ distinct contracts/libraries/interfaces |
| Findings | 0 (expected; no EVM rule pack yet) |
What the extractor produces
Per scanned file:
| Vertex / edge | Source | Notes |
|---|---|---|
VertexKind::Service |
contract X / library X / interface X / abstract contract X |
attributes.contract_kind carries the keyword; contract_name carries the identifier. |
VertexKind::Function |
function f(...) / modifier m(...) / constructor / fallback / receive |
Attributes: solidity_kind, function_name, contract_name (parent), visibility, mutability, modifiers. |
EdgeKind::DependsOn |
contract Child is A, B(args) |
One edge per parent. Parents may resolve to stub vertices (cross-file references aren't tracked yet). |
EdgeKind::DependsOn |
contract ↔ function structural attribution | Every function gets a containment edge from its enclosing contract. |
EdgeKind::Calls |
function body → known in-file function | Keyword-blocklisted (if/require/emit/etc.). Cross-file calls resolve only when the callee was also extracted in this scan. |
Top-of-graph snapshot for the tBTC v2 BOB clone:
Service vertices (sample): AccessControl, BurnFromMintTokenPoolUpgradeable,
BurnMintERC20Mock, ERC20, ERC20BurnableUpgradeable, ERC20Mock,
ERC20PermitUpgradeable, ERC20Upgradeable, ERC721, ERC721Mock,
IBurnMintERC20Upgradeable, IERC165, IERC165Upgradeable, IERC20Upgradeable,
ILegacyMintableERC20, ILiquidityContainer, IOptimismMintableERC20,
IPoolV1, IRMN, IRouter, ISemver, ITypeAndVersion, Initializable,
L2TBTC, LockReleaseTokenPoolUpgradeable, MockRouter,
OptimismMintableUpgradableERC20, OptimismMintableUpgradableTBTC,
Pausable, Pool, RateLimiter, ReentrancyGuard, SafeERC20, Timelock,
TokenPoolUpgradeable, ...
Functions per contract (top 5):
39 TokenPoolUpgradeable
19 OptimismMintableUpgradableTBTC
16 L2TBTC
11 OptimismMintableUpgradableERC20
11 LockReleaseTokenPoolUpgradeable
Why not tree-sitter
Every published tree-sitter-solidity release pins a tree-sitter core version (^0.20, ^0.24, ^0.25) that conflicts with this workspace's tree-sitter = "0.22" pin, producing duplicate tree_sitter::Language types at compile time (same class of issue documented in Cargo.toml for tree-sitter-css 0.23). The crate uses a focused regex pass instead.
The trade-off is documented in code/cli/crates/pinpoint-lang-solidity/src/lib.rs:
- The extractor reliably handles standard Solidity syntax (contracts, libraries, interfaces, abstract contracts, function/modifier/constructor/fallback/receive, inheritance with constructor-args, modifier invocations, visibility/mutability keywords).
- Out of scope until a real AST: state variable extraction, event declarations, assembly blocks, cross-file imports, proxy patterns, and any rule that needs scope-aware AST traversal.
When tree-sitter core is bumped workspace-wide (or a 0.22-compatible grammar release lands), the regex pass can be swapped for tree-sitter with no rule-pack churn — the vertex/edge shape is grammar-agnostic.
What's still missing for a real bounty submission
pinpoint-fw-evmframework crate — recognize OpenZeppelin / proxy patterns (UUPS, Transparent, Beacon), Chainlink CCIP token-pool conventions, AccessControl role declarations.- Expand
pinpoint-rules-evmbeyond the 3 initial detectors (see below). Mining targets: reentrancy, oracle-stale, signature-malleability, unbounded-mint, missing-events, etc. - State-variable extraction — most EVM access-control rules need to see
mapping(address => bool) public isAdminstyle state, not just function signatures. - Body inspection — recognize inline
require(msg.sender == ...)/if (msg.sender != ...) revertpatterns to suppress the EVM-AUTH-001 false positives on production tBTC v2 BOB code.
For the Threshold bounty today, conventional tools (Slither, Mythril, Echidna, Semgrep + Solidity rules, Certora for the mint/burn formal model) remain the right call.
EVM rule pack — initial 3 detectors (shipped 2026-05-19)
| Rule | Severity | What it fires on |
|---|---|---|
EVM-AUTH-001 |
High | external/public function whose name matches a privileged pattern (mint/burn/pause/upgrade/withdraw/set*/role-management/initialize/...) with no recognized access-control modifier in the modifiers attribute. |
EVM-INIT-001 |
High | initialize/init/__<Module>_init function declared public/external without an initializer / reinitializer / onlyInitializing modifier. Classic UUPS hijack class. |
EVM-UPGRADE-001 |
Critical | Contract inheriting UUPSUpgradeable (or 1967-line equivalent) that does not declare _authorizeUpgrade in the same source file. Triage signal: the single-file extractor cannot follow imports to a parent override. |
All three skip findings on test/ / tests/ / _test.sol mock paths via the shared is_test_path helper (from pinpoint-rules-coverage-quality).
Validation against tBTC v2 BOB contracts
Scan with --profile all --min-confidence 0.0:
=== Findings by Rule ===
QUAL-001: 23
QUAL-003: 2
Zero EVM rule fires. EVM-AUTH-001 initially flagged 3 production functions (LockReleaseTokenPoolUpgradeable.withdrawLiquidity, TokenPoolUpgradeable.setChainRateLimiterConfigs, TokenPoolUpgradeable.setChainRateLimiterConfig), all of which gate access inline via if (msg.sender != X) revert Unauthorized(...) / require(msg.sender == owner, ...) rather than via a modifier. The follow-up body_snippet extractor pass + inline-msg.sender suppressor (see commit history) clears all 3.
EVM-INIT-001 and EVM-UPGRADE-001 also did not fire on this codebase; the Threshold contracts use the OpenZeppelin pattern correctly (every initializer carries initializer / reinitializer, every UUPS contract overrides _authorizeUpgrade). Precision on this codebase across all three EVM rules: 100% (0 false positives, 0 known false negatives).
The 23 QUAL-001 (dead code) and 2 QUAL-003 (complexity) fires are coverage/quality signals from the pre-existing rule pack and are not security findings.
Reproduction
# from repo root
cargo build --release --features internal -p pinpoint-cli
# clone target
git clone --depth 1 https://github.com/threshold-network/tbtc-v2.git /tmp/tbtc-v2
# scan
pinpoint spectre scan /tmp/tbtc-v2/cross-chain/bob/contracts \
--local --output summary
Code references
| Concern | Path |
|---|---|
| Solidity extractor | code/cli/crates/pinpoint-lang-solidity/src/lib.rs |
| Workspace member | code/cli/Cargo.toml (added under Wave 1 language crates) |
| CLI feature flags | code/cli/crates/pinpoint-cli/Cargo.toml (lang-solidity, evm bundle) |
| Registration | code/cli/crates/pinpoint-cli/src/main.rs (build_registry, behind #[cfg(feature = "lang-solidity")]) |