@elytrasec/engine 0.3.2 → 0.4.1

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.
Files changed (3) hide show
  1. package/dist/index.js +417 -44
  2. package/package.json +13 -9
  3. package/dist/index.d.ts +0 -1358
package/dist/index.js CHANGED
@@ -2957,12 +2957,43 @@ var securityRules = [
2957
2957
  title: "Hardcoded secret or credential",
2958
2958
  description: "A string that looks like a password, API key, or token is hardcoded in source code.",
2959
2959
  suggestion: "Move the secret to an environment variable or a secrets manager. Rotate the exposed credential immediately.",
2960
- pattern: /(?:password|secret|api_key|apikey|api_secret|token|auth_token|private_key|client_secret)\s*[:=]\s*["'](?!changeme|password|your-|<|TODO|xxx|test|example|placeholder|REPLACE)[^"']{8,}["']/i,
2960
+ // `token` removed from the keyword list — too ambiguous in crypto context (tokenAddress,
2961
+ // token0, token1, tokenContract, tokenFactory all match and are public addresses, not secrets).
2962
+ // `auth_token`, `accessToken`, `bearerToken`, `jwt_token` etc. remain via specific patterns below.
2963
+ pattern: /(?:password|secret|apikey|api_key|api_secret|auth[_-]?token|access[_-]?token|bearer[_-]?token|jwt[_-]?secret|private[_-]?key|client_secret)\s*[:=]\s*["'](?!changeme|password|your-|<|TODO|xxx|test|example|placeholder|REPLACE|0x[0-9a-fA-F]{40}["'])[^"']{8,}["']/i,
2961
2964
  severity: "critical",
2962
2965
  category: "security",
2963
2966
  confidence: "medium",
2964
2967
  languages: ALL
2965
2968
  },
2969
+ {
2970
+ // Crypto-native: a raw 32-byte (64-hex) value assigned to a *private*-key-shaped name.
2971
+ // Wallet/contract addresses (40 hex) are public — never flag those.
2972
+ // Hashes / UIDs / merkle roots (64 hex) on a `*Hash`, `*Uid`, `*Root` LHS are public — don't flag.
2973
+ // Only flag when the LHS specifically suggests a SIGNING KEY.
2974
+ id: "cp-sec-private-key-hex",
2975
+ title: "Hardcoded EVM private key",
2976
+ description: "A 64-character hex value is assigned to a variable whose name implies it's a signing key. If real, this is a wallet private key \u2014 anyone who reads the source can drain the wallet.",
2977
+ suggestion: "Move to an environment variable, KMS, or hardware wallet immediately. Rotate the key (transfer all assets to a new wallet and never use this key again).",
2978
+ pattern: /\b(?:private[_-]?key|signing[_-]?key|wallet[_-]?key|signer[_-]?key|deployer[_-]?key|mnemonic[_-]?seed|raw[_-]?key|eas_private_key|x402_wallet_key)\s*[:=]\s*["']?0x[0-9a-fA-F]{64}["']?/i,
2979
+ severity: "critical",
2980
+ category: "security",
2981
+ confidence: "high",
2982
+ languages: ALL
2983
+ },
2984
+ {
2985
+ // BIP-39 seed phrase — 12 or 24 lowercase English words in a row.
2986
+ // Any leak of these = full wallet compromise. Targets the common assignment pattern.
2987
+ id: "cp-sec-mnemonic-phrase",
2988
+ title: "Hardcoded BIP-39 mnemonic / seed phrase",
2989
+ description: "A string with 12 or 24 lowercase words looks like a BIP-39 mnemonic. A leaked seed phrase compromises the entire HD wallet and every account derived from it.",
2990
+ suggestion: "Never store mnemonics in source. Use a secure secret manager. If exposed, immediately transfer all assets out of every derived account, never reuse the seed.",
2991
+ pattern: /(?:mnemonic|seed[_-]?phrase|secret[_-]?phrase|backup[_-]?phrase)\s*[:=]\s*["'](?:[a-z]{3,8}\s+){11,23}[a-z]{3,8}["']/i,
2992
+ severity: "critical",
2993
+ category: "security",
2994
+ confidence: "high",
2995
+ languages: ALL
2996
+ },
2966
2997
  {
2967
2998
  id: "cp-sec-hardcoded-ip",
2968
2999
  title: "Hardcoded IP address",
@@ -3074,7 +3105,7 @@ var securityRules = [
3074
3105
  title: "Form without CSRF protection",
3075
3106
  description: "HTML form with method=POST but no visible CSRF token field. This may be vulnerable to cross-site request forgery.",
3076
3107
  suggestion: "Add a hidden CSRF token field to the form or use a framework-provided CSRF middleware.",
3077
- pattern: /method\s*=\s*["']post["'][^>]*>(?![\s\S]{0,500}csrf)/i,
3108
+ multilinePattern: /method\s*=\s*["']post["'][^>]*>(?![\s\S]{0,500}csrf)/i,
3078
3109
  severity: "medium",
3079
3110
  category: "security",
3080
3111
  confidence: "low",
@@ -3130,7 +3161,7 @@ var securityRules = [
3130
3161
  title: "XML parsing without entity restriction (XXE)",
3131
3162
  description: "XML parsers that allow external entities can be exploited to read local files or perform SSRF.",
3132
3163
  suggestion: "Disable external entity processing: set noent: false, or use defusedxml in Python.",
3133
- pattern: /(?:parseXml|parseString|DOMParser|xml2js|libxmljs|XMLParser)\s*\((?![\s\S]*(?:noent:\s*false|resolve_entities:\s*false))/,
3164
+ multilinePattern: /(?:parseXml|parseString|DOMParser|xml2js|libxmljs|XMLParser)\s*\((?![\s\S]*(?:noent:\s*false|resolve_entities:\s*false))/,
3134
3165
  severity: "high",
3135
3166
  category: "security",
3136
3167
  confidence: "medium",
@@ -3180,7 +3211,7 @@ var securityRules = [
3180
3211
  title: "Mass assignment via spread of user input",
3181
3212
  description: "Spreading req.body directly into a database create/update allows attackers to set arbitrary fields (e.g. isAdmin).",
3182
3213
  suggestion: "Explicitly pick allowed fields instead of spreading the entire request body.",
3183
- pattern: /(?:create|update|insert|save|upsert)\s*\(\s*\{[\s\S]{0,50}\.\.\.(?:req\.body|req\.query|body|input)\b/,
3214
+ multilinePattern: /(?:create|update|insert|save|upsert)\s*\(\s*\{[\s\S]{0,50}\.\.\.(?:req\.body|req\.query|body|input)\b/,
3184
3215
  severity: "high",
3185
3216
  category: "security",
3186
3217
  confidence: "medium",
@@ -3247,7 +3278,7 @@ var securityRules = [
3247
3278
  title: "Cookie set without security flags",
3248
3279
  description: "Cookies set without Secure, HttpOnly, or SameSite flags are vulnerable to interception and XSS theft.",
3249
3280
  suggestion: "Set cookies with { secure: true, httpOnly: true, sameSite: 'strict' } flags.",
3250
- pattern: /(?:res\.cookie|setCookie|set-cookie|document\.cookie)\s*(?:\(|=)(?![\s\S]{0,100}(?:secure|httpOnly|SameSite))/i,
3281
+ multilinePattern: /(?:res\.cookie|setCookie|set-cookie|document\.cookie)\s*(?:\(|=)(?![\s\S]{0,100}(?:secure|httpOnly|SameSite))/i,
3251
3282
  severity: "medium",
3252
3283
  category: "security",
3253
3284
  confidence: "medium",
@@ -3275,7 +3306,7 @@ var securityRules = [
3275
3306
  title: "CORS credentials with wildcard origin",
3276
3307
  description: "Enabling credentials with a wildcard or reflected origin allows any site to make authenticated cross-origin requests.",
3277
3308
  suggestion: "When using credentials: true, specify explicit trusted origins instead of '*' or reflecting the Origin header.",
3278
- pattern: /credentials\s*:\s*true[\s\S]{0,200}origin\s*:\s*(?:["']\*["']|req\.headers\.origin|true)/,
3309
+ multilinePattern: /credentials\s*:\s*true[\s\S]{0,200}origin\s*:\s*(?:["']\*["']|req\.headers\.origin|true)/,
3279
3310
  severity: "high",
3280
3311
  category: "security",
3281
3312
  confidence: "high",
@@ -3309,13 +3340,13 @@ var securityRules = [
3309
3340
  var solidityRules2 = [
3310
3341
  {
3311
3342
  id: "cp-sol-reentrancy",
3312
- title: "Potential reentrancy \u2014 external call before state update",
3313
- description: "An external call (call, send, transfer) appears before a state variable assignment. An attacker contract can re-enter before state is updated.",
3314
- suggestion: "Follow the checks-effects-interactions pattern: update state before making external calls. Consider using ReentrancyGuard.",
3315
- pattern: /\.(?:call|send|transfer)\s*[\({]/,
3316
- severity: "critical",
3343
+ title: "Potential reentrancy \u2014 review external call sequencing",
3344
+ description: "An external call via `.call{value:...}` is present. The classic reentrancy pattern requires the call to occur BEFORE a state update \u2014 a same-line regex can't verify the order across multiple lines, so this is flagged for manual or AI review. `.send` and `.transfer` are intentionally not flagged because their 2300-gas stipend prevents reentrancy except in pathological cases (post-EIP-1884 gas changes).",
3345
+ suggestion: "Follow checks-effects-interactions: update state before external calls. Or use OpenZeppelin's ReentrancyGuard. Confirm by reading the surrounding function or running AI deep-review.",
3346
+ pattern: /\.call\s*\{(?:[^}]*value[^}]*)?\}/,
3347
+ severity: "high",
3317
3348
  category: "security",
3318
- confidence: "medium",
3349
+ confidence: "low",
3319
3350
  languages: SOL
3320
3351
  },
3321
3352
  {
@@ -3399,12 +3430,12 @@ var solidityRules2 = [
3399
3430
  {
3400
3431
  id: "cp-sol-assembly",
3401
3432
  title: "Inline assembly usage",
3402
- description: "Inline assembly bypasses Solidity's safety checks (overflow, type-safety). Bugs are easy to introduce and hard to audit.",
3403
- suggestion: "Use Solidity builtins where possible. If assembly is required, document it thoroughly and add extensive tests.",
3433
+ description: "Inline assembly bypasses Solidity's safety checks. Routine in production DeFi for gas optimization (transient storage, packed structs, math) \u2014 flagged informationally so reviewers can verify the assembly block is justified.",
3434
+ suggestion: "Confirm the assembly block is necessary and audited. Document the storage slots / opcodes used.",
3404
3435
  pattern: /\bassembly\s*\{/,
3405
- severity: "medium",
3406
- category: "security",
3407
- confidence: "high",
3436
+ severity: "info",
3437
+ category: "solidity",
3438
+ confidence: "low",
3408
3439
  languages: SOL
3409
3440
  },
3410
3441
  {
@@ -3421,11 +3452,11 @@ var solidityRules2 = [
3421
3452
  {
3422
3453
  id: "cp-sol-unchecked-math",
3423
3454
  title: "Unchecked arithmetic block",
3424
- description: "The `unchecked` block disables overflow/underflow checks. This is dangerous unless you have proven the values cannot overflow.",
3425
- suggestion: "Only use unchecked blocks when overflow is mathematically impossible. Add comments explaining the safety invariant.",
3455
+ description: "`unchecked` disables Solidity 0.8+ overflow checks. Usually intentional for gas savings on loop counters and proven-safe math, but worth confirming each block has a documented invariant.",
3456
+ suggestion: "Add a comment explaining why overflow is impossible in this block. Consider whether the gas savings justify the risk.",
3426
3457
  pattern: /unchecked\s*\{/,
3427
- severity: "medium",
3428
- category: "security",
3458
+ severity: "low",
3459
+ category: "solidity",
3429
3460
  confidence: "low",
3430
3461
  languages: SOL
3431
3462
  },
@@ -3446,7 +3477,7 @@ var solidityRules2 = [
3446
3477
  title: "balanceOf used for pricing without flash loan guards",
3447
3478
  description: "Using balanceOf() for pricing calculations is vulnerable to flash loan manipulation.",
3448
3479
  suggestion: "Use time-weighted average prices (TWAP) or Chainlink oracles instead of spot balanceOf for pricing.",
3449
- pattern: /balanceOf\s*\([^)]*\)[\s\S]{0,20}(?:\*|\/)/,
3480
+ multilinePattern: /balanceOf\s*\([^)]*\)[\s\S]{0,20}(?:\*|\/)/,
3450
3481
  severity: "high",
3451
3482
  category: "security",
3452
3483
  confidence: "medium",
@@ -3455,25 +3486,25 @@ var solidityRules2 = [
3455
3486
  {
3456
3487
  id: "cp-sol-storage-collision",
3457
3488
  title: "delegatecall with state variables \u2014 storage collision risk",
3458
- description: "Using delegatecall in a contract with state variables can cause storage layout collisions with the implementation contract.",
3489
+ description: "Using delegatecall in a contract with state variables can cause storage layout collisions with the implementation contract. Common in proxy patterns where it's intentional \u2014 verify EIP-1967 storage slots are used. High false-positive rate on standard proxy implementations.",
3459
3490
  suggestion: "Use EIP-1967 proxy pattern with standardized storage slots, or ensure identical storage layouts.",
3460
3491
  pattern: /delegatecall\s*\(/,
3461
3492
  multilinePattern: /(?:uint|int|address|mapping|bytes|string|bool)\s+(?:public|private|internal)?\s*\w+[\s\S]{0,2000}delegatecall\s*\(/g,
3462
- severity: "high",
3493
+ severity: "medium",
3463
3494
  category: "security",
3464
- confidence: "medium",
3495
+ confidence: "low",
3465
3496
  languages: SOL
3466
3497
  },
3467
3498
  {
3468
3499
  id: "cp-sol-missing-event",
3469
3500
  title: "State change without event emission",
3470
- description: "State-changing functions should emit events for off-chain indexing and transparency.",
3471
- suggestion: "Add an event emission after state changes for important variables.",
3501
+ description: "State-changing functions should emit events for off-chain indexing. Best practice for indexers and UI, not a security vulnerability \u2014 flagged informationally.",
3502
+ suggestion: "Add an event emission after state changes if downstream indexers / UI need to track them.",
3472
3503
  pattern: /(?!)/,
3473
3504
  // never matches per-line; use multiline only
3474
3505
  multilinePattern: /function\s+\w+\s*\([^)]*\)\s*(?:external|public)[^{]*\{(?:(?!emit\s)[\s\S]){1,500}\w+\s*=[^=]/g,
3475
- severity: "medium",
3476
- category: "security",
3506
+ severity: "info",
3507
+ category: "quality",
3477
3508
  confidence: "low",
3478
3509
  languages: SOL
3479
3510
  }
@@ -3506,7 +3537,7 @@ var javaRules = [
3506
3537
  title: "XPath injection via string concatenation",
3507
3538
  description: "Building XPath queries with string concatenation allows injection attacks.",
3508
3539
  suggestion: "Use XPath parameterization via XPathExpression or sanitize input.",
3509
- pattern: /(?:XPath|xpath)[\s\S]{0,50}\.(?:evaluate|compile)\s*\(\s*(?:["'][^"']*["']\s*\+|.*\+\s*["'])/,
3540
+ multilinePattern: /(?:XPath|xpath)[\s\S]{0,50}\.(?:evaluate|compile)\s*\(\s*(?:["'][^"']*["']\s*\+|.*\+\s*["'])/,
3510
3541
  severity: "high",
3511
3542
  category: "security",
3512
3543
  confidence: "medium",
@@ -3900,7 +3931,7 @@ var performanceRules = [
3900
3931
  title: "Potential N+1 query \u2014 database call inside a loop",
3901
3932
  description: "A database query inside a loop makes N separate round-trips instead of 1 batch query. This causes severe performance degradation at scale.",
3902
3933
  suggestion: "Batch the queries: collect all IDs first, then execute a single WHERE IN query.",
3903
- pattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?(?:\.find\(|\.findOne\(|\.findUnique\(|\.query\(|\.execute\(|SELECT\b)/,
3934
+ multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?(?:\.find\(|\.findOne\(|\.findUnique\(|\.query\(|\.execute\(|SELECT\b)/,
3904
3935
  severity: "high",
3905
3936
  category: "performance",
3906
3937
  confidence: "medium",
@@ -3911,7 +3942,7 @@ var performanceRules = [
3911
3942
  title: "Nested iteration \u2014 Array search inside a loop",
3912
3943
  description: "Using Array.find/filter/some/indexOf inside a loop is O(n*m). Use a Map or Set for O(n) lookups.",
3913
3944
  suggestion: "Build a Map or Set from the inner array before the loop, then use .get()/.has() for O(1) lookup.",
3914
- pattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?\.(?:find|filter|some|indexOf|includes)\s*\(/,
3945
+ multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?\.(?:find|filter|some|indexOf|includes)\s*\(/,
3915
3946
  severity: "low",
3916
3947
  category: "performance",
3917
3948
  confidence: "medium",
@@ -3934,7 +3965,7 @@ var performanceRules = [
3934
3965
  title: "RegExp construction inside a loop",
3935
3966
  description: "Creating a new RegExp object on every iteration is wasteful. Regex compilation is expensive.",
3936
3967
  suggestion: "Move the RegExp construction outside the loop and reuse the compiled pattern.",
3937
- pattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?new\s+RegExp\s*\(/,
3968
+ multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?new\s+RegExp\s*\(/,
3938
3969
  severity: "low",
3939
3970
  category: "performance",
3940
3971
  confidence: "medium",
@@ -3945,7 +3976,7 @@ var performanceRules = [
3945
3976
  title: "JSON.parse inside a loop",
3946
3977
  description: "Parsing JSON inside a loop is CPU-intensive. If the same data is re-parsed, cache the result.",
3947
3978
  suggestion: "Parse the JSON once before the loop and reuse the result, or use streaming JSON parsing for large datasets.",
3948
- pattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?JSON\.parse\s*\(/,
3979
+ multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?JSON\.parse\s*\(/,
3949
3980
  severity: "low",
3950
3981
  category: "performance",
3951
3982
  confidence: "medium",
@@ -3956,7 +3987,7 @@ var performanceRules = [
3956
3987
  title: "Sequential await inside a loop",
3957
3988
  description: "Using await inside a loop executes async operations sequentially. Independent operations can run in parallel with Promise.all.",
3958
3989
  suggestion: "Collect promises in an array and use Promise.all() or Promise.allSettled() for parallel execution.",
3959
- pattern: /(?:for|while)\s*\([\s\S]*?await\s+/,
3990
+ multilinePattern: /(?:for|while)\s*\([\s\S]*?await\s+/,
3960
3991
  severity: "low",
3961
3992
  category: "performance",
3962
3993
  confidence: "low",
@@ -4615,12 +4646,12 @@ var eip7702Rules = [
4615
4646
  {
4616
4647
  id: "cp-sol-7702-unguarded-init",
4617
4648
  title: "EIP-7702 delegation: unguarded initializer",
4618
- description: "Contracts used as EIP-7702 delegation targets must guard initializers \u2014 constructors don't run on delegated EOAs. An unguarded `initialize()` can be front-run, letting an attacker take ownership of the delegated account.",
4619
- suggestion: "Add an `initializer` modifier (OpenZeppelin) or an `initialized` flag checked at the top of every init function. Ensure no init path is callable twice.",
4649
+ description: "Contracts intended as EIP-7702 delegation targets must guard initializers \u2014 constructors don't run on delegated EOAs. An unguarded `initialize()` can be front-run, letting an attacker take ownership. The pattern also matches generic proxy initializers, which usually ARE guarded (just not by an `initializer` keyword on the same line), so confirm whether this contract is actually a 7702 target before treating as critical.",
4650
+ suggestion: "If this is a 7702 delegation target: add `initializer` modifier (OpenZeppelin) or a boolean guard. For regular proxies, confirm the initializer is gated by the proxy factory.",
4620
4651
  pattern: /function\s+initialize\s*\([^)]*\)\s*(?:external|public)(?!\s+\w*[Ii]nitializer)/,
4621
- severity: "critical",
4652
+ severity: "high",
4622
4653
  category: "solidity",
4623
- confidence: "medium",
4654
+ confidence: "low",
4624
4655
  languages: SOL
4625
4656
  },
4626
4657
  {
@@ -4688,7 +4719,7 @@ var uniswapV4Rules = [
4688
4719
  title: "Uniswap v4 hook: BalanceDelta returned without settle/take",
4689
4720
  description: "Hooks that return a modified `BalanceDelta` must ensure the delta is fully consumed (settled or taken) within the same unlock cycle. An unconsumed delta causes `CurrencyNotSettled` revert; partial consumption can silently strand tokens.",
4690
4721
  suggestion: "Ensure `poolManager.settle()` or `poolManager.take()` is called for both currency0 and currency1 before returning from the unlock callback.",
4691
- pattern: /returns\s*\([^)]*BalanceDelta[^)]*\)(?![\s\S]{0,800}?(?:settle|\.take)\s*\()/,
4722
+ multilinePattern: /returns\s*\([^)]*BalanceDelta[^)]*\)(?![\s\S]{0,800}?(?:settle|\.take)\s*\()/,
4692
4723
  severity: "high",
4693
4724
  category: "solidity",
4694
4725
  confidence: "low",
@@ -4699,7 +4730,7 @@ var uniswapV4Rules = [
4699
4730
  title: "Uniswap v4 hook: overly broad hook permissions",
4700
4731
  description: "Hook permissions are set at deployment via the address bit-flags and cannot be changed. Requesting permissions you don't need (e.g. `BEFORE_SWAP_RETURNS_DELTA` when you never return a delta) increases attack surface and gas costs for every pool interaction.",
4701
4732
  suggestion: "Return only the minimum required `Hooks.Permissions` from `getHookPermissions()`. Audit each permission flag against your actual callback implementations.",
4702
- pattern: /getHookPermissions\s*\(\s*\)\s*(?:public|external|override)[^{]*\{[\s\S]{0,300}?true/,
4733
+ multilinePattern: /getHookPermissions\s*\(\s*\)\s*(?:public|external|override)[^{]*\{[\s\S]{0,300}?true/,
4703
4734
  severity: "medium",
4704
4735
  category: "solidity",
4705
4736
  confidence: "low",
@@ -4717,9 +4748,337 @@ var uniswapV4Rules = [
4717
4748
  languages: SOL
4718
4749
  }
4719
4750
  ];
4751
+ var hackReplayRules = [
4752
+ /* ── Curve Finance ($73M, July 2023) ────────────────────────── */
4753
+ /* Root cause: Vyper compiler versions 0.2.15, 0.2.16, 0.3.0 */
4754
+ /* generated broken reentrancy locks — the malloc-style storage */
4755
+ /* slot for the lock was not reused correctly across functions. */
4756
+ {
4757
+ id: "cp-hack-curve-vyper-version",
4758
+ title: "Curve hack pattern \u2014 Vyper compiler version with broken reentrancy lock",
4759
+ description: "This file declares Vyper 0.2.15, 0.2.16, or 0.3.0 \u2014 the exact compiler versions that mis-generated reentrancy locks and enabled the July 2023 Curve Finance exploit ($73M across multiple pools). The bug is in the compiler, not your source code: any @nonreentrant guard in these versions is silently bypassable.",
4760
+ suggestion: "Upgrade to Vyper >= 0.3.1 (lock fix landed in 0.3.1) and redeploy any affected pools. If you cannot redeploy, pause all reentrancy-sensitive functions and migrate liquidity.",
4761
+ pattern: /#\s*@version\s+0\.(?:2\.1[56]|3\.0)(?:\s|$)/,
4762
+ severity: "critical",
4763
+ category: "hack-replay",
4764
+ confidence: "high",
4765
+ languages: [".vy"]
4766
+ },
4767
+ /* ── Euler Finance ($197M, March 2023) ──────────────────────── */
4768
+ /* Root cause: donateToReserves() allowed an attacker to push */
4769
+ /* their own account into an artificially unhealthy state, */
4770
+ /* then self-liquidate at a discount via the liquidation flow. */
4771
+ {
4772
+ id: "cp-hack-euler-donate-self-liquidation",
4773
+ title: "Euler hack pattern \u2014 donate function reachable from liquidation path",
4774
+ description: "A donate/donateTo* function in a lending or vault context can be weaponized to artificially worsen the caller's health factor, then trigger self-liquidation at a profit. This is the exact vector that drained $197M from Euler Finance in March 2023.",
4775
+ suggestion: "Either remove the donate function, or (a) require donations to come from accounts with no active borrows, (b) recompute health AFTER the donation as if the donated assets remained the donor's, and (c) prevent same-block liquidation of the donor.",
4776
+ pattern: /function\s+donate[A-Za-z]*\s*\(/,
4777
+ severity: "high",
4778
+ category: "hack-replay",
4779
+ confidence: "medium",
4780
+ languages: SOL
4781
+ },
4782
+ /* ── Radiant Capital ($53M, October 2024) ───────────────────── */
4783
+ /* Root cause: 3-of-11 multisig members signed a malicious */
4784
+ /* transferOwnership tx (their UI was tampered by malware). */
4785
+ /* A two-step transferOwnership + timelock would have given the */
4786
+ /* team a chance to notice and abort. */
4787
+ {
4788
+ id: "cp-hack-radiant-immediate-ownership-transfer",
4789
+ title: "Radiant hack pattern \u2014 single-step ownership transfer (no Ownable2Step)",
4790
+ description: "transferOwnership is implemented as a single-call function \u2014 the new owner takes effect immediately. This is the same pattern the Radiant Capital ($53M, Oct 2024) attackers exploited after compromising a multisig signer's UI: ownership flipped before anyone could react. A two-step or timelocked pattern would have created a defensive window.",
4791
+ suggestion: "Inherit from OpenZeppelin Ownable2Step (requires acceptOwnership from the new owner) AND gate it behind a timelock for production deployments. Never let a single signature transfer ownership atomically.",
4792
+ multilinePattern: /function\s+transferOwnership\s*\(\s*address\s+\w+\s*\)\s*(?:public|external)(?![\s\S]{0,400}?(?:pendingOwner|_pendingOwner|acceptOwnership|Ownable2Step|timelock|Timelock|TimelockController))/,
4793
+ severity: "high",
4794
+ category: "hack-replay",
4795
+ confidence: "medium",
4796
+ languages: SOL
4797
+ },
4798
+ /* ── zkSync Airdrop ($5M, April 2025) ───────────────────────── */
4799
+ /* Root cause: sweepUnclaimed() admin-only function with no */
4800
+ /* timelock or multisig gate — when the admin key was */
4801
+ /* compromised, attacker minted 111M ZK in a single tx. */
4802
+ /* Pattern targets the unambiguous red-flag names. We do NOT */
4803
+ /* flag plain `mint` since regular token contracts use it with */
4804
+ /* proper guards — single-line regex can't verify a timelock */
4805
+ /* check on the next line, so we stay conservative. */
4806
+ {
4807
+ id: "cp-hack-zksync-admin-sweep-no-timelock",
4808
+ title: "zkSync hack pattern \u2014 admin-only sweep/drain/rescue function",
4809
+ description: "An admin-gated function with a name like sweep*, drain*, rescue*, emergencyWithdraw*, forceMint*, adminMint* is the exact category of function the zkSync airdrop attacker exploited (April 2025, $5M loss). Even with a timelock check inside, these functions are high-value targets \u2014 a compromised admin key bypasses everything. The pattern is reviewed manually because a same-line regex cannot verify whether a timelock guard is enforced.",
4810
+ suggestion: "Audit this function carefully: (a) is it gated by a TimelockController contract address check, not just an onlyOwner modifier? (b) is the action bounded (max amount per call, per epoch)? (c) is there an N-of-M multisig requirement at the modifier level? If any answer is no, add it before mainnet.",
4811
+ pattern: /function\s+(?:sweep[A-Za-z]*|drain[A-Za-z]*|rescue[A-Za-z]*|emergencyWithdraw[A-Za-z]*|forceMint[A-Za-z]*|adminMint[A-Za-z]*)\s*\([^)]*\)\s*(?:external|public)/,
4812
+ severity: "high",
4813
+ category: "hack-replay",
4814
+ confidence: "medium",
4815
+ languages: SOL
4816
+ },
4817
+ /* ── Bybit ($1.46B, February 2025) ──────────────────────────── */
4818
+ /* Root cause: Safe{Wallet}'s frontend bundle on S3 was modified */
4819
+ /* to display a legit tx while signing a malicious one to */
4820
+ /* hardware wallets. This is a CI/CD + frontend integrity issue. */
4821
+ {
4822
+ id: "cp-hack-bybit-unpinned-action-checkout",
4823
+ title: "Bybit hack pattern \u2014 unpinned GitHub Action handling deploy/release",
4824
+ description: "A workflow that deploys frontend bundles or release artifacts uses a third-party action pinned to a mutable ref (@main, @master, or a version tag instead of a commit SHA). This is the supply-chain vector behind the $1.46B Bybit hack (Feb 2025): the deploy pipeline trusts a moving target. An attacker that compromises the action's repo can swap your build output.",
4825
+ suggestion: "Pin every third-party action to a full 40-char commit SHA (e.g. `uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab`). Use Dependabot or Renovate to keep SHAs up to date with review.",
4826
+ pattern: /uses:\s+(?!actions\/)[^\s]+@(?:main|master|v\d+(?:\.\d+)*)\s*$/m,
4827
+ severity: "high",
4828
+ category: "hack-replay",
4829
+ confidence: "medium",
4830
+ languages: [],
4831
+ pathPattern: /\.github\/workflows\/.*\.ya?ml$/
4832
+ },
4833
+ /* ── Beanstalk ($182M, April 2022) ──────────────────────────── */
4834
+ /* Root cause: governance proposal could call arbitrary code in */
4835
+ /* the same transaction it was submitted/voted-on, with voting */
4836
+ /* power based on token balance at proposal time. Attacker flash- */
4837
+ /* loaned BEAN, voted YES on a self-draining proposal, executed */
4838
+ /* immediately. No timelock between vote and execution. */
4839
+ {
4840
+ id: "cp-hack-beanstalk-instant-governance",
4841
+ title: "Beanstalk hack pattern \u2014 governance execute() with no timelock between vote and call",
4842
+ description: "A governance contract exposes an execute() / executeProposal() / propose() function callable in the same block as voting. This is the exact $182M Beanstalk vector (April 2022): an attacker flash-loaned the governance token, voted yes on a self-draining proposal, and called execute() in the same transaction. A timelock between successful vote and execution would have made the flash-loan economically pointless.",
4843
+ suggestion: "Add a queue/execute split: successful proposals enter a TimelockController with a minimum delay (24-48h is standard). Require execute() to verify block.timestamp >= queuedAt + delay. Never let voting power, proposal acceptance, and code execution happen atomically.",
4844
+ multilinePattern: /function\s+(?:execute(?:Proposal)?|propose(?:AndExecute)?)\s*\([^)]*\)\s*(?:external|public)(?:\s+payable)?(?:\s+returns)?[^{]*\{(?![\s\S]{0,600}?(?:timelock|Timelock|TimelockController|queued|block\.timestamp\s*[><]=?\s*\w+\s*\+))/,
4845
+ severity: "critical",
4846
+ category: "hack-replay",
4847
+ confidence: "low",
4848
+ languages: SOL
4849
+ },
4850
+ /* ── Multichain ($126M, July 2023) ──────────────────────────── */
4851
+ /* Root cause: bridge admin private keys controlled by a single */
4852
+ /* person (CEO). When that person was detained, funds were */
4853
+ /* withdrawn from MPC-controlled addresses. Single key controls */
4854
+ /* bridge withdraw / unlock = single point of failure. */
4855
+ {
4856
+ id: "cp-hack-multichain-single-admin-bridge",
4857
+ title: "Multichain hack pattern \u2014 bridge withdraw/unlock gated by single owner role",
4858
+ description: "A bridge function (withdraw, unlock, releaseTo, claim, mintBridged) is gated only by onlyOwner or a single-address admin check. This was the structural failure behind the $126M Multichain collapse (July 2023): the CEO's MPC key controlled bridge outflows, and when he was detained, attackers (or insiders) drained the bridge. Bridge withdraw functions are the highest-value attack surface in crypto and must be multi-party.",
4859
+ suggestion: "Require a multi-sig (Safe / Gnosis), a multi-party MPC (TSS), or an N-of-M validator set before any bridge outflow. Never let a single private key sign a withdraw of bridged assets. Add per-asset and per-block outflow caps as a defense-in-depth limit.",
4860
+ multilinePattern: /function\s+(?:withdraw|unlock|releaseTo|releaseFor|claim|mintBridged|bridgeOut|relayOut)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin|onlyMPC)(?![\s\S]{0,200}?(?:multisig|Multisig|threshold|N_OF_M|validators|attestors))/,
4861
+ severity: "critical",
4862
+ category: "hack-replay",
4863
+ confidence: "medium",
4864
+ languages: SOL
4865
+ },
4866
+ /* ── Ronin Bridge ($625M, March 2022) ───────────────────────── */
4867
+ /* Root cause: 5-of-9 validator set with 4 validators run by */
4868
+ /* Sky Mavis + 1 by Axie DAO. Attacker compromised the 4 Sky */
4869
+ /* Mavis keys and the Axie DAO had previously granted Sky Mavis */
4870
+ /* signing rights "temporarily" but never revoked → 5/9 reached. */
4871
+ /* Detection: low signature threshold relative to validator set. */
4872
+ {
4873
+ id: "cp-hack-ronin-low-validator-threshold",
4874
+ title: "Ronin hack pattern \u2014 bridge validator threshold \u2264 5/9 or hardcoded low quorum",
4875
+ description: "A bridge contract uses a hardcoded signature threshold of 4, 5, or 6 \u2014 the same range that allowed the $625M Ronin bridge hack (March 2022). Sky Mavis ran 4 of 9 validators; combined with one improperly-revoked delegated key, the attacker hit the 5/9 threshold from a single team's compromise. A higher threshold (7-of-9, 8-of-11) makes single-team compromise insufficient.",
4876
+ suggestion: "Audit your validator threshold against the social/operational concentration of validator operators. Aim for \u2265 70% threshold (e.g. 7-of-9, 11-of-15). Periodically rotate delegated signing rights and verify revocations actually happen. Set up monitoring for validator-set changes.",
4877
+ pattern: /(?:signatureThreshold|requiredSignatures|threshold|quorum|VALIDATOR_THRESHOLD)\s*[=:]\s*(?:uint(?:8|16|32|256)\s*\(\s*)?[4-6]\b/i,
4878
+ severity: "high",
4879
+ category: "hack-replay",
4880
+ confidence: "low",
4881
+ languages: SOL
4882
+ },
4883
+ /* ── Cream Finance ($130M, October 2021) ────────────────────── */
4884
+ /* Root cause: oracle manipulated via flash-loaned yUSD vault */
4885
+ /* shares. Collateral price computed from spot share supply, */
4886
+ /* inflatable via flash loan → borrow against inflated value → */
4887
+ /* drain. Detection: spot-balance arithmetic in pricing without */
4888
+ /* TWAP / Chainlink fallback. */
4889
+ {
4890
+ id: "cp-hack-cream-spot-share-pricing",
4891
+ title: "Cream hack pattern \u2014 collateral price computed from spot vault share supply",
4892
+ description: "A function computes a token/share price by dividing total assets by spot totalSupply (or balanceOf this). The $130M Cream hack (Oct 2021) exploited exactly this on yUSDVault: flash-loan inflated the share supply during a single tx, the price oracle read the manipulated value, and the attacker borrowed massively against fictitious collateral. Spot supply/balance ratios are flash-loan-attackable.",
4893
+ suggestion: "Use Chainlink price feeds with staleness checks, a TWAP from a deep Uniswap V3 pool, or external collateral-pricing oracles. Never compute price from same-block totalSupply or balanceOf.",
4894
+ pattern: /(?:totalSupply|balanceOf)\s*\([^)]*\)\s*[*/]\s*\w+|\w+\s*[*/]\s*(?:totalSupply|balanceOf)\s*\(/,
4895
+ severity: "high",
4896
+ category: "hack-replay",
4897
+ confidence: "low",
4898
+ languages: SOL
4899
+ },
4900
+ /* ── Wormhole ($325M, February 2022) ────────────────────────── */
4901
+ /* Root cause: signature verification path used a stub guardian */
4902
+ /* set lookup that didn't validate the set hash. Attacker forged */
4903
+ /* a guardian signature against an empty set, which the contract */
4904
+ /* accepted because the set comparison was wrong. Detection: */
4905
+ /* ecrecover or signature-set membership without strict hash */
4906
+ /* equality of the validator set. */
4907
+ {
4908
+ id: "cp-hack-wormhole-unchecked-signature-set",
4909
+ title: "Wormhole hack pattern \u2014 guardian/validator signature accepted without strict set verification",
4910
+ description: "A function verifies a signature against a validator/guardian set but does not strictly check that the set hash matches the expected current set (or compares against a default/empty value). The $325M Wormhole hack (Feb 2022) exploited a stub `verify_signatures` path that accepted forged signatures because the validator set comparison was missing/insecure. ecrecover-based signature schemes need every validator set rotation tracked by an explicit hash check.",
4911
+ suggestion: "Verify the signed message includes a strict hash of the current validator set. Reject signatures where the recovered signer is not in the active set. Never use `default!()` or zero-initialized guardian arrays in signature paths.",
4912
+ multilinePattern: /(?:ecrecover|recover)\s*\([\s\S]{0,400}?\)(?![\s\S]{0,300}?(?:guardianSet|validatorSet|currentSetHash|setHash|require\s*\([^)]*==\s*expected))/,
4913
+ severity: "critical",
4914
+ category: "hack-replay",
4915
+ confidence: "low",
4916
+ languages: SOL
4917
+ },
4918
+ /* ── Nomad Bridge ($190M, August 2022) ──────────────────────── */
4919
+ /* Root cause: an upgrade initialized the trusted-root mapping */
4920
+ /* with 0x00. Any message with a 0-bytes proof became 'proven'. */
4921
+ /* Anyone could replay arbitrary messages against the bridge. */
4922
+ /* Detection: trusted-root / merkle-root mapping reads or writes */
4923
+ /* that don't reject zero values. */
4924
+ {
4925
+ id: "cp-hack-nomad-zero-root-acceptance",
4926
+ title: "Nomad hack pattern \u2014 trusted/merkle root lookup accepts zero hash as valid",
4927
+ description: "A function reads a value from a merkle-root or trusted-root mapping and treats any non-revert response as valid, including the zero default. The $190M Nomad bridge hack (Aug 2022) was caused by an upgrade that initialized the root mapping to zero \u2014 every message proof of zero became 'valid', and the entire bridge was drained by 300+ copy-paste attackers within hours.",
4928
+ suggestion: "Always `require(root != bytes32(0))` before treating a stored root/hash as a valid proof anchor. Apply the same check to any zero-value default in upgrade-initialized storage.",
4929
+ pattern: /(?:roots|messages|acceptableRoot|trustedRoot|merkleRoot)\s*\[[^\]]+\]\s*(?:==\s*(?:true|0x01)|!=\s*0x00\b)(?!\s*&&\s*\w+\s*!=\s*(?:bytes32\(0\)|0x0+))/,
4930
+ severity: "critical",
4931
+ category: "hack-replay",
4932
+ confidence: "low",
4933
+ languages: SOL
4934
+ },
4935
+ /* ── Mango Markets ($114M, October 2022) ────────────────────── */
4936
+ /* Root cause: oracle was a thinly-traded spot AMM price. */
4937
+ /* Attacker pumped MNGO token via CEX wash trades, then borrowed */
4938
+ /* $114M against inflated collateral. Detection: pricing pulled */
4939
+ /* from a single low-liquidity source with no sanity bounds. */
4940
+ {
4941
+ id: "cp-hack-mango-single-source-oracle",
4942
+ title: "Mango hack pattern \u2014 oracle uses single price source without bounds",
4943
+ description: "A pricing function reads from a single oracle source (one Uniswap pool, one CEX feed, one Chainlink aggregator) without sanity bounds or a secondary source. The $114M Mango Markets hack (Oct 2022) was a textbook oracle-manipulation attack: the attacker pumped MNGO price ~10\xD7 via CEX trades, the oracle dutifully reported it, and Mango let them borrow $114M against the now-inflated collateral.",
4944
+ suggestion: "Use at least two independent oracle sources for any collateral pricing. Apply a circuit breaker (max % change per block, deviation from TWAP). For long-tail tokens, cap borrowable collateral or require multi-source confirmation.",
4945
+ // Tightened: require the call to appear in a function whose name implies
4946
+ // lending / collateral / liquidation context (the actual hack surface).
4947
+ // Plain DEX pool internals like Uniswap getReserves are not at risk.
4948
+ multilinePattern: /function\s+(?:borrow\w*|liquidat\w*|isHealthy|collateralValue|getAccountHealth|maxBorrow|withdrawCollateral|getBorrowable)[\s\S]{0,600}?\b(?:getPrice|latestAnswer|peek|read)\s*\((?![\s\S]{0,400}?(?:twap|TWAP|deviation|maxDeviation|stalenessThreshold|secondaryPrice|sanity))/,
4949
+ severity: "medium",
4950
+ category: "hack-replay",
4951
+ confidence: "low",
4952
+ languages: SOL
4953
+ }
4954
+ ];
4955
+ var rugSurfaceRules = [
4956
+ {
4957
+ id: "cp-rug-owner-can-mint",
4958
+ title: "Owner can mint tokens",
4959
+ description: "A mint function is gated by onlyOwner (or similar single-address modifier). Owner can dilute holders at will. Verify whether this is intentional (e.g. emissions schedule) or unrestricted.",
4960
+ suggestion: "Hard-cap max supply, route mints through a timelock + multisig, or renounce ownership after initial mint.",
4961
+ pattern: /function\s+(?:_?mint|mintTo|airdrop)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin|onlyMinter)/,
4962
+ severity: "high",
4963
+ category: "rug-surface",
4964
+ confidence: "medium",
4965
+ languages: SOL
4966
+ },
4967
+ {
4968
+ id: "cp-rug-owner-can-pause",
4969
+ title: "Owner can pause transfers",
4970
+ description: "Owner can call _pause() or set a paused flag \u2014 halting all transfers. Useful for emergencies, dangerous if the owner is an EOA without timelock.",
4971
+ suggestion: "Move pause behind multisig + timelock. Document expected pause scenarios. Consider auto-unpause after N blocks.",
4972
+ pattern: /function\s+(?:pause|setPaused|_pause|emergencyPause)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin)/,
4973
+ severity: "medium",
4974
+ category: "rug-surface",
4975
+ confidence: "medium",
4976
+ languages: SOL
4977
+ },
4978
+ {
4979
+ id: "cp-rug-owner-can-blacklist",
4980
+ title: "Owner can blacklist addresses",
4981
+ description: "Owner can prevent specific addresses from transferring. Honeypot pattern when combined with no transfer-tax limit or a hidden blacklist mapping.",
4982
+ suggestion: "Either remove the blacklist entirely (rely on protocol-level sanctions), or gate it behind a multisig + timelock with public allowlist process.",
4983
+ pattern: /(?:blacklist|denylist|isBlocked|blocked)\s*\[\s*\w+\s*\]\s*=\s*(?:true|1)|function\s+(?:blacklist|setBlacklist|addToBlacklist|block(?:Address|User|Account))\s*\(/i,
4984
+ severity: "high",
4985
+ category: "rug-surface",
4986
+ confidence: "medium",
4987
+ languages: SOL
4988
+ },
4989
+ {
4990
+ id: "cp-rug-owner-can-change-fees",
4991
+ title: "Owner can change transfer fees",
4992
+ description: "A setFee / updateFee / setTax function gated by onlyOwner. Owner can spike fees to 99% effectively stopping trading.",
4993
+ suggestion: "Cap maximum fee in the contract (e.g. require new fee <= 10%). Combine with a timelock so changes are observable in advance.",
4994
+ pattern: /function\s+(?:setFee|setTax|setBuyFee|setSellFee|setTransferFee|updateFee|updateTax|setFees)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin)/i,
4995
+ severity: "high",
4996
+ category: "rug-surface",
4997
+ confidence: "medium",
4998
+ languages: SOL
4999
+ },
5000
+ {
5001
+ id: "cp-rug-owner-can-change-router",
5002
+ title: "Owner can swap router or pair",
5003
+ description: "A setRouter / setPair / setSwapRouter function gated by onlyOwner. Owner can redirect liquidity to an attacker-controlled router and drain trades.",
5004
+ suggestion: "Make router / pair immutable. If upgrades are needed, gate behind multisig + 7-day timelock with clear public announcement.",
5005
+ pattern: /function\s+(?:setRouter|setPair|setSwapRouter|updateRouter|setUniswapRouter|setSwapPair)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin)/i,
5006
+ severity: "high",
5007
+ category: "rug-surface",
5008
+ confidence: "medium",
5009
+ languages: SOL
5010
+ },
5011
+ {
5012
+ id: "cp-rug-owner-can-sweep",
5013
+ title: "Owner can sweep ETH or ERC20 from contract",
5014
+ description: 'Owner can call a sweep / rescue / withdraw / recover function that drains arbitrary tokens or ETH from the contract. Often labelled "rescue stuck funds" \u2014 but the same path drains user-owned balances.',
5015
+ suggestion: "Restrict to specific tokens that should never be in the contract. Exclude the protocol's own LP token / treasury. Behind multisig + timelock.",
5016
+ pattern: /function\s+(?:rescueTokens|rescueERC20|sweepTokens|sweep|sweepERC20|recoverTokens|recoverERC20|withdrawStuck|withdrawTokens|emergencyWithdraw|emergencyRescue)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin)/i,
5017
+ severity: "critical",
5018
+ category: "rug-surface",
5019
+ confidence: "medium",
5020
+ languages: SOL
5021
+ },
5022
+ {
5023
+ id: "cp-rug-owner-can-upgrade",
5024
+ title: "Proxy upgrade gated by single owner",
5025
+ description: "An _upgradeTo / upgradeTo function is callable by onlyOwner (UUPS pattern). The implementation contract can be swapped to attacker code unilaterally.",
5026
+ suggestion: "Route upgrades through a TimelockController owned by a multisig. Publish the new implementation address before the timelock expires.",
5027
+ pattern: /function\s+(?:_upgradeTo|upgradeTo|upgradeToAndCall|_authorizeUpgrade)\s*\([^)]*\)\s*(?:external|public|internal|virtual\s+override)?(?:[^{]*?)(?:onlyOwner|onlyAdmin)/,
5028
+ severity: "high",
5029
+ category: "rug-surface",
5030
+ confidence: "medium",
5031
+ languages: SOL
5032
+ },
5033
+ {
5034
+ id: "cp-rug-role-self-grant",
5035
+ title: "Role admin can grant itself any role",
5036
+ description: "DEFAULT_ADMIN_ROLE or a self-managed role can call grantRole() on itself or on other privileged roles. Single admin compromise = full takeover.",
5037
+ suggestion: "Use AccessControlEnumerable + revoke admin's ability to manage its own role. Move sensitive roles to a separate multisig.",
5038
+ pattern: /_setupRole\s*\(\s*DEFAULT_ADMIN_ROLE|_grantRole\s*\(\s*DEFAULT_ADMIN_ROLE|grantRole\s*\([^,]+,\s*msg\.sender\s*\)/,
5039
+ severity: "high",
5040
+ category: "rug-surface",
5041
+ confidence: "medium",
5042
+ languages: SOL
5043
+ },
5044
+ {
5045
+ id: "cp-rug-no-renounce-evidence",
5046
+ title: "Renounceable ownership without evidence of renunciation",
5047
+ description: "Contract inherits Ownable / OwnableUpgradeable but the constructor / initializer doesn't transfer ownership to a known-safe address (zero, multisig, timelock). Owner power may still be active.",
5048
+ suggestion: "Either renounce ownership in the constructor (acceptably for fully-immutable contracts), or transfer to a multisig. Document this explicitly.",
5049
+ multilinePattern: /(?:contract|abstract\s+contract)\s+\w+[\s\S]{0,500}?\bis\s+[\w,\s]*?Ownable(?:Upgradeable)?\b(?![\s\S]{0,2000}?(?:renounceOwnership\s*\(\)|transferOwnership\s*\(\s*address\(0x|TimelockController|Safe\s*\())/,
5050
+ severity: "medium",
5051
+ category: "rug-surface",
5052
+ confidence: "low",
5053
+ languages: SOL
5054
+ },
5055
+ {
5056
+ id: "cp-rug-max-wallet-or-tx",
5057
+ title: "Trading restrictions (max wallet / max tx)",
5058
+ description: "Contract enforces maxWallet, maxTransactionAmount, or similar caps. Common in memecoin templates \u2014 verify the cap can't be set to 0 or 1 wei (effectively halting trading).",
5059
+ suggestion: "Ensure max-cap setters have a minimum floor (e.g. 0.1% of supply). Combine with timelock for changes after launch.",
5060
+ pattern: /(?:maxWallet|maxTransactionAmount|maxTx|maxWalletAmount|_maxWallet|_maxTx)\s*[:=]/i,
5061
+ severity: "low",
5062
+ category: "rug-surface",
5063
+ confidence: "medium",
5064
+ languages: SOL
5065
+ },
5066
+ {
5067
+ id: "cp-rug-hidden-modifier",
5068
+ title: "Privileged modifier without visible role check in body",
5069
+ description: "A modifier name doesn't suggest privilege (e.g. `lock`, `safe`, `nonReentrant`-shaped names) but its body restricts access. Auditors miss these; users assume the function is open.",
5070
+ suggestion: "Rename modifier to make privilege explicit (e.g. `onlyOperator`). Or move check inline so reviewers see it in the function body.",
5071
+ multilinePattern: /modifier\s+(?!onlyOwner|onlyAdmin|onlyRole|nonReentrant|whenNotPaused|whenPaused|initializer|reinitializer)(\w+)\s*\(\s*\)\s*\{[\s\S]{0,200}?require\s*\(\s*msg\.sender\s*==\s*\w+/,
5072
+ severity: "medium",
5073
+ category: "rug-surface",
5074
+ confidence: "low",
5075
+ languages: SOL
5076
+ }
5077
+ ];
4720
5078
  var ALL_RULES2 = [
4721
5079
  ...securityRules,
4722
5080
  ...solidityRules2,
5081
+ ...rugSurfaceRules,
4723
5082
  ...javaRules,
4724
5083
  ...rubyRules,
4725
5084
  ...phpRules,
@@ -4730,7 +5089,8 @@ var ALL_RULES2 = [
4730
5089
  ...iacRules,
4731
5090
  ...eip7702Rules,
4732
5091
  ...tstoreRules,
4733
- ...uniswapV4Rules
5092
+ ...uniswapV4Rules,
5093
+ ...hackReplayRules
4734
5094
  ];
4735
5095
 
4736
5096
  // src/static/pattern-scanner.ts
@@ -4782,9 +5142,13 @@ function isJsxFile(filePath) {
4782
5142
  function isScriptDir(filePath) {
4783
5143
  return /(?:^|\/)(?:scripts|build|tools|infra)\//i.test(filePath);
4784
5144
  }
5145
+ function isVendorFile(filePath) {
5146
+ return /(?:^|\/)(?:node_modules|@openzeppelin|@uniswap|@aave|@chainlink|solmate|solady|lib|vendor|dependencies|forge-std|ds-test)(?:\/|@)/i.test(filePath);
5147
+ }
4785
5148
  function isFalsePositive(ctx) {
4786
5149
  if (isDocsFile(ctx.filePath)) return true;
4787
- if (ctx.rule.id !== "cp-qual-todo-fixme" && isCommentLine(ctx.line)) return true;
5150
+ if (isVendorFile(ctx.filePath)) return true;
5151
+ if (ctx.rule.id !== "cp-qual-todo-fixme" && ctx.rule.category !== "hack-replay" && isCommentLine(ctx.line)) return true;
4788
5152
  if (isImportLine(ctx.line) && /(?:http-no-tls|hardcoded-ip|hardcoded-secret)/.test(ctx.rule.id)) return true;
4789
5153
  if (isConfigFile(ctx.filePath) && ctx.rule.category === "security") return true;
4790
5154
  const trimmed = ctx.line.trim();
@@ -4802,6 +5166,11 @@ function isFalsePositive(ctx) {
4802
5166
  if (/process\.env\b/.test(ctx.line)) return true;
4803
5167
  }
4804
5168
  }
5169
+ if (ctx.rule.id === "cp-sec-hardcoded-secret") {
5170
+ if (/=\s*["']?0x[0-9a-fA-F]{40}["']?/.test(ctx.line)) return true;
5171
+ if (/\b(?:address|token0?|token1|tokenAddress|contractAddress|pool|router|factory|vault|recipient|payTo|treasury|owner|admin|operator|signer|delegate|merkleRoot|root|domain|salt|chainId|deadline|attestation|uid|schema)\b\s*[:=]/i.test(ctx.line)) return true;
5172
+ if (/\b(?:hash|sha|keccak|merkle|root|uid|tx|proof|signature|sig|domain|salt|nonce|attestation|commitment)\w*\s*[:=]\s*["']?0x[0-9a-fA-F]{64}/i.test(ctx.line)) return true;
5173
+ }
4805
5174
  return false;
4806
5175
  }
4807
5176
  function adjustSeverityForContext(severity, filePath) {
@@ -4939,7 +5308,7 @@ ${lines[i]}`,
4939
5308
  }
4940
5309
  }
4941
5310
  }
4942
- if (lines.length > 8) {
5311
+ if (lines.length > 8 && !SOL2.includes(ext)) {
4943
5312
  const WINDOW = 4;
4944
5313
  const blockMap = /* @__PURE__ */ new Map();
4945
5314
  for (let i = 0; i <= lines.length - WINDOW; i++) {
@@ -5026,6 +5395,7 @@ function scanFile(relPath, content, rules, changedRanges) {
5026
5395
  const applicableRules = rules.filter((r) => ruleAppliesToFile(r, relPath));
5027
5396
  if (applicableRules.length === 0) return findings;
5028
5397
  for (const rule of applicableRules) {
5398
+ if (!rule.pattern) continue;
5029
5399
  for (let i = 0; i < lines.length; i++) {
5030
5400
  const lineNumber = i + 1;
5031
5401
  const line = lines[i];
@@ -5057,8 +5427,10 @@ function scanFile(relPath, content, rules, changedRanges) {
5057
5427
  if (rule.id === "cp-clean-callback-hell" && isTestFile(relPath)) continue;
5058
5428
  if (rule.id === "cp-sec-command-injection" && isScriptDir(relPath)) continue;
5059
5429
  rule.multilinePattern.lastIndex = 0;
5430
+ const isGlobal = rule.multilinePattern.flags.includes("g");
5060
5431
  let match;
5061
5432
  while ((match = rule.multilinePattern.exec(content)) !== null) {
5433
+ const breakAfter = !isGlobal;
5062
5434
  const textBefore = content.slice(0, match.index);
5063
5435
  const startLine = textBefore.split("\n").length;
5064
5436
  const matchLines = match[0].split("\n").length;
@@ -5084,6 +5456,7 @@ function scanFile(relPath, content, rules, changedRanges) {
5084
5456
  if (match[0].length === 0) {
5085
5457
  rule.multilinePattern.lastIndex++;
5086
5458
  }
5459
+ if (breakAfter) break;
5087
5460
  }
5088
5461
  }
5089
5462
  if (rules.some((r) => r.category === "code-cleaning")) {