solidity-argus 0.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.
Files changed (131) hide show
  1. package/AGENTS.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +249 -0
  4. package/package.json +43 -0
  5. package/skills/INVENTORY.md +79 -0
  6. package/skills/README.md +56 -0
  7. package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +424 -0
  8. package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +157 -0
  9. package/skills/checklists/cyfrin-defi-core/SKILL.md +373 -0
  10. package/skills/checklists/cyfrin-defi-integrations/SKILL.md +412 -0
  11. package/skills/checklists/cyfrin-gas/SKILL.md +55 -0
  12. package/skills/checklists/general-audit/SKILL.md +433 -0
  13. package/skills/methodology/audit-workflow/SKILL.md +129 -0
  14. package/skills/methodology/report-template/SKILL.md +190 -0
  15. package/skills/methodology/severity-classification/SKILL.md +179 -0
  16. package/skills/protocol-patterns/amm-dex/SKILL.md +229 -0
  17. package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +317 -0
  18. package/skills/protocol-patterns/dao-governance/SKILL.md +281 -0
  19. package/skills/protocol-patterns/lending-borrowing/SKILL.md +221 -0
  20. package/skills/protocol-patterns/staking-vesting/SKILL.md +247 -0
  21. package/skills/references/exploit-reference/SKILL.md +259 -0
  22. package/skills/references/smartbugs-examples/SKILL.md +296 -0
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +298 -0
  24. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +59 -0
  25. package/skills/vulnerability-patterns/assert-violation/SKILL.md +59 -0
  26. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +61 -0
  27. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +55 -0
  28. package/skills/vulnerability-patterns/default-visibility/SKILL.md +62 -0
  29. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +60 -0
  30. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +59 -0
  31. package/skills/vulnerability-patterns/dos-revert/SKILL.md +72 -0
  32. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +249 -0
  33. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +51 -0
  34. package/skills/vulnerability-patterns/hash-collision/SKILL.md +52 -0
  35. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +61 -0
  36. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +60 -0
  37. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +59 -0
  38. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +61 -0
  39. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +61 -0
  40. package/skills/vulnerability-patterns/logic-errors/SKILL.md +333 -0
  41. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +60 -0
  42. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +66 -0
  43. package/skills/vulnerability-patterns/off-by-one/SKILL.md +67 -0
  44. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +252 -0
  45. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +65 -0
  46. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +61 -0
  47. package/skills/vulnerability-patterns/reentrancy/SKILL.md +266 -0
  48. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +72 -0
  49. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +59 -0
  50. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +63 -0
  51. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +52 -0
  52. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +65 -0
  53. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +61 -0
  54. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +63 -0
  55. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +56 -0
  56. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +80 -0
  57. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +69 -0
  58. package/skills/vulnerability-patterns/unused-variables/SKILL.md +70 -0
  59. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +81 -0
  60. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +77 -0
  61. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +294 -0
  62. package/src/agents/argus-prompt.ts +407 -0
  63. package/src/agents/pythia-prompt.ts +134 -0
  64. package/src/agents/scribe-prompt.ts +87 -0
  65. package/src/agents/sentinel-prompt.ts +133 -0
  66. package/src/cli/cli-program.ts +67 -0
  67. package/src/cli/commands/doctor.ts +83 -0
  68. package/src/cli/commands/init.ts +46 -0
  69. package/src/cli/commands/install.ts +55 -0
  70. package/src/cli/index.ts +13 -0
  71. package/src/cli/tui-prompts.ts +75 -0
  72. package/src/cli/types.ts +9 -0
  73. package/src/config/index.ts +3 -0
  74. package/src/config/loader.ts +36 -0
  75. package/src/config/schema.ts +82 -0
  76. package/src/config/types.ts +4 -0
  77. package/src/constants/defaults.ts +6 -0
  78. package/src/create-hooks.ts +84 -0
  79. package/src/create-managers.ts +26 -0
  80. package/src/create-tools.ts +30 -0
  81. package/src/features/audit-enforcer/audit-enforcer.ts +34 -0
  82. package/src/features/audit-enforcer/index.ts +1 -0
  83. package/src/features/background-agent/background-manager.ts +200 -0
  84. package/src/features/background-agent/index.ts +1 -0
  85. package/src/features/context-monitor/context-monitor.ts +48 -0
  86. package/src/features/context-monitor/index.ts +4 -0
  87. package/src/features/context-monitor/tool-output-truncator.ts +17 -0
  88. package/src/features/error-recovery/index.ts +2 -0
  89. package/src/features/error-recovery/session-recovery.ts +27 -0
  90. package/src/features/error-recovery/tool-error-recovery.ts +35 -0
  91. package/src/features/index.ts +5 -0
  92. package/src/features/persistent-state/audit-state-manager.ts +121 -0
  93. package/src/features/persistent-state/index.ts +1 -0
  94. package/src/hooks/compaction-hook.ts +50 -0
  95. package/src/hooks/config-handler.ts +116 -0
  96. package/src/hooks/event-hook-v2.ts +93 -0
  97. package/src/hooks/event-hook.ts +74 -0
  98. package/src/hooks/hook-system.ts +9 -0
  99. package/src/hooks/index.ts +5 -0
  100. package/src/hooks/knowledge-sync-hook.ts +57 -0
  101. package/src/hooks/safe-create-hook.ts +15 -0
  102. package/src/hooks/system-prompt-hook.ts +126 -0
  103. package/src/hooks/tool-tracking-hook.ts +234 -0
  104. package/src/hooks/types.ts +16 -0
  105. package/src/index.ts +36 -0
  106. package/src/knowledge/scvd-client.ts +242 -0
  107. package/src/knowledge/scvd-index.ts +183 -0
  108. package/src/knowledge/scvd-sync.ts +85 -0
  109. package/src/managers/index.ts +1 -0
  110. package/src/managers/types.ts +85 -0
  111. package/src/plugin-interface.ts +38 -0
  112. package/src/shared/binary-utils.ts +63 -0
  113. package/src/shared/deep-merge.ts +71 -0
  114. package/src/shared/file-utils.ts +56 -0
  115. package/src/shared/index.ts +5 -0
  116. package/src/shared/jsonc-parser.ts +39 -0
  117. package/src/shared/logger.ts +36 -0
  118. package/src/state/audit-state.ts +27 -0
  119. package/src/state/finding-store.ts +126 -0
  120. package/src/state/plugin-state.ts +14 -0
  121. package/src/state/types.ts +61 -0
  122. package/src/tools/contract-analyzer-tool.ts +184 -0
  123. package/src/tools/forge-fuzz-tool.ts +311 -0
  124. package/src/tools/forge-test-tool.ts +397 -0
  125. package/src/tools/pattern-checker-tool.ts +337 -0
  126. package/src/tools/report-generator-tool.ts +308 -0
  127. package/src/tools/slither-tool.ts +465 -0
  128. package/src/tools/solodit-search-tool.ts +131 -0
  129. package/src/tools/sync-knowledge-tool.ts +116 -0
  130. package/src/utils/project-detector.ts +133 -0
  131. package/src/utils/solidity-parser.ts +174 -0
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: shadowing-state-variables
3
+ description: - Contract inherits from one or more parent contracts
4
+ ---
5
+ <!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
6
+
7
+ # Shadowing State Variables
8
+
9
+ ## Preconditions
10
+ - Contract inherits from one or more parent contracts
11
+ - A state variable in the child contract has the same name as a state variable in a parent contract
12
+ - Solidity version <0.6.0 (>=0.6.0 disallows state variable shadowing with a compiler error)
13
+ - OR: function parameters or local variables shadow state variables (any Solidity version)
14
+
15
+ ## Vulnerable Pattern
16
+ ```solidity
17
+ contract Base {
18
+ address public owner;
19
+
20
+ constructor() {
21
+ owner = msg.sender;
22
+ }
23
+ }
24
+
25
+ // Solidity <0.6.0: this compiles without error
26
+ contract Child is Base {
27
+ address public owner; // Shadows Base.owner — creates a NEW variable
28
+
29
+ function setOwner(address _owner) external {
30
+ owner = _owner; // Sets Child.owner, NOT Base.owner
31
+ }
32
+
33
+ // Base's functions still read Base.owner (the original)
34
+ // Child's functions read Child.owner (the shadow)
35
+ // These are two different storage variables!
36
+ }
37
+
38
+ // Local variable shadowing (any version)
39
+ contract Example {
40
+ uint256 public value = 100;
41
+
42
+ function getValue() public view returns (uint256) {
43
+ uint256 value = 0; // Shadows state variable
44
+ return value; // Returns 0, not 100
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Detection Heuristics
50
+ 1. Check Solidity version: if <0.6.0, search for state variables in child contracts that share names with parent contract variables
51
+ 2. For any version, search for function parameters and local variables that share names with state variables
52
+ 3. Check compiler warnings — modern Solidity warns about local shadowing even if it allows it
53
+ 4. For each shadowed variable, trace which version (parent's or child's) each function reads/writes — inconsistency indicates a bug
54
+ 5. Pay special attention to `owner`, `admin`, and other access-control variables being shadowed
55
+
56
+ ## False Positives
57
+ - Solidity >=0.6.0: state variable shadowing is a compiler error, so it can't happen
58
+ - Local variable shadowing where the intent is clear and the state variable is not needed in that scope (still bad practice but not exploitable)
59
+ - The shadowing is in a test file or non-production code
60
+
61
+ ## Remediation
62
+ - Upgrade to Solidity >=0.6.0 where state variable shadowing is a compiler error
63
+ - For local variable shadowing, use distinct names (e.g., prefix with `_` for parameters)
64
+ - Rename conflicting variables to be unique across the inheritance chain
65
+ ```solidity
66
+ contract Child is Base {
67
+ // Don't redeclare — use Base.owner directly
68
+ function setOwner(address _newOwner) external {
69
+ owner = _newOwner; // Modifies Base.owner
70
+ }
71
+ }
72
+ ```
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: signature-malleability
3
+ description: - Contract uses ECDSA signatures for authorization or deduplication
4
+ ---
5
+ <!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
6
+
7
+ # Signature Malleability
8
+
9
+ ## Preconditions
10
+ - Contract uses ECDSA signatures for authorization or deduplication
11
+ - Signatures are tracked by their raw bytes (e.g., `mapping(bytes => bool)`) to prevent replay
12
+ - No enforcement that the `s` value is in the lower half of the curve order
13
+
14
+ ## Vulnerable Pattern
15
+ ```solidity
16
+ mapping(bytes => bool) public usedSignatures;
17
+
18
+ function claimReward(bytes memory signature, uint256 amount) external {
19
+ // Deduplication by raw signature bytes
20
+ require(!usedSignatures[signature], "already used");
21
+
22
+ bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount));
23
+ address signer = ecrecover(hash, v, r, s);
24
+ require(signer == trustedSigner);
25
+
26
+ usedSignatures[signature] = true; // Attacker submits (r, n-s, flipped_v)
27
+ // to bypass this check with a valid but different signature
28
+ _payout(msg.sender, amount);
29
+ }
30
+ ```
31
+
32
+ ## Detection Heuristics
33
+ 1. Search for `mapping(bytes => bool)` or any mapping keyed by raw signature bytes
34
+ 2. If signatures are used as mapping keys for deduplication, flag it — an attacker can compute the complementary `(r, n-s)` signature
35
+ 3. Check if `ecrecover` is called directly without an `s`-value range check
36
+ 4. Check if OpenZeppelin's ECDSA library is used (it enforces lower-s normalization)
37
+ 5. If neither a library nor a manual `s < secp256k1n/2` check is present, flag it
38
+
39
+ ## False Positives
40
+ - Signatures are deduplicated by message hash or nonce, not by raw signature bytes
41
+ - OpenZeppelin's `ECDSA.recover` is used, which rejects high-s signatures
42
+ - Manual check enforces `s <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0`
43
+
44
+ ## Remediation
45
+ - Track used signatures by message hash or nonce, not by raw signature bytes
46
+ - Use OpenZeppelin's `ECDSA.recover` which enforces `s` in the lower half of the curve order
47
+ - If using raw `ecrecover`, add a manual `s`-value check
48
+ ```solidity
49
+ // Use nonce-based deduplication instead of signature bytes
50
+ mapping(address => uint256) public nonces;
51
+
52
+ function claimReward(uint8 v, bytes32 r, bytes32 s, uint256 amount) external {
53
+ uint256 nonce = nonces[msg.sender]++;
54
+ bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount, nonce));
55
+ address signer = ECDSA.recover(hash, v, r, s); // Rejects malleable sigs
56
+ require(signer == trustedSigner);
57
+ _payout(msg.sender, amount);
58
+ }
59
+ ```
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: unbounded-return-data
3
+ description: - Contract makes a low-level `.call()` to an untrusted or user-specified address
4
+ ---
5
+ <!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
6
+
7
+ # Unbounded Return Data
8
+
9
+ ## Preconditions
10
+ - Contract makes a low-level `.call()` to an untrusted or user-specified address
11
+ - Solidity's automatic return data copying is used (default behavior up to at least v0.8.26)
12
+ - No assembly-level restriction on `returndatacopy` size
13
+ - The function is on a critical path where revert would lock funds (withdrawals, undelegation)
14
+
15
+ ## Vulnerable Pattern
16
+ ```solidity
17
+ function unstake(address callback) external {
18
+ uint256 amount = stakes[msg.sender];
19
+ stakes[msg.sender] = 0;
20
+
21
+ // Solidity automatically copies ALL return data into memory
22
+ // Attacker's callback contract returns megabytes of data
23
+ // Memory expansion cost grows quadratically — causes out-of-gas
24
+ (bool success,) = callback.call(
25
+ abi.encodeWithSignature("onUnstake(uint256)", amount)
26
+ );
27
+ // Even with limited gas stipend, the return data copy
28
+ // happens in the CALLER's gas context
29
+ require(success, "callback failed");
30
+ }
31
+ ```
32
+
33
+ ## Detection Heuristics
34
+ 1. Search for `.call(`, `.delegatecall(`, `.staticcall(` to addresses that could be attacker-controlled
35
+ 2. Check if the return data is handled by Solidity's default (captured as `bytes memory` or discarded but still copied)
36
+ 3. If the call target is untrusted and no assembly-level return data size limit is used, flag it
37
+ 4. Check if the function is on a critical path — can a revert here lock user funds?
38
+ 5. Look for callback patterns (delegation hooks, unstaking callbacks, flash loan callbacks) where the callee is attacker-controlled
39
+
40
+ ## False Positives
41
+ - The call target is a trusted, known contract (not user-controlled)
42
+ - Assembly is used to limit `returndatacopy` to a bounded size (e.g., max 32 bytes)
43
+ - `ExcessivelySafeCall` or similar library is used for bounded return data
44
+ - The function doesn't revert on call failure (uses try/catch or ignores success)
45
+
46
+ ## Remediation
47
+ - Use assembly to limit return data size instead of Solidity's automatic copy
48
+ - Use Nomad's `ExcessivelySafeCall` library for calls to untrusted addresses
49
+ - Bound `returndatacopy` to only the bytes you need (typically 0 or 32)
50
+ ```solidity
51
+ // Assembly-bounded return data copy
52
+ function safeCall(address target, bytes memory data) internal returns (bool success) {
53
+ assembly {
54
+ success := call(gas(), target, 0, add(data, 0x20), mload(data), 0, 0)
55
+ // Only copy up to 32 bytes of return data
56
+ let rdsize := returndatasize()
57
+ if gt(rdsize, 0) {
58
+ if gt(rdsize, 32) { rdsize := 32 }
59
+ returndatacopy(0, 0, rdsize)
60
+ }
61
+ }
62
+ }
63
+ ```
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: unchecked-return-values
3
+ description: - Contract uses low-level calls: `.call()`, `.send()`, or `.delegatecall()`
4
+ ---
5
+ <!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
6
+
7
+ # Unchecked Return Values
8
+
9
+ ## Preconditions
10
+ - Contract uses low-level calls: `.call()`, `.send()`, or `.delegatecall()`
11
+ - The boolean return value indicating success/failure is not checked
12
+ - State changes occur after the unchecked call, assuming it succeeded
13
+
14
+ ## Vulnerable Pattern
15
+ ```solidity
16
+ function withdraw(uint256 amount) external {
17
+ // .send() returns false on failure but does NOT revert
18
+ msg.sender.send(amount); // Return value ignored
19
+ balances[msg.sender] -= amount; // State updated even if send failed
20
+ }
21
+
22
+ function payout(address to, uint256 amount) external {
23
+ // .call() return value captured but never checked
24
+ (bool success,) = to.call{value: amount}("");
25
+ // success could be false, but execution continues
26
+ totalPaid += amount;
27
+ }
28
+ ```
29
+
30
+ ## Detection Heuristics
31
+ 1. Search for all `.call(`, `.send(`, `.delegatecall(` invocations
32
+ 2. For each, check whether the returned boolean is captured AND checked (e.g., `require(success)`, `if (!success) revert`)
33
+ 3. If the return value is captured but never referenced again, flag it
34
+ 4. If the return value is not captured at all (e.g., bare `addr.send(amount);`), flag it
35
+ 5. Check for state changes after unchecked calls — these create inconsistent state on silent failure
36
+
37
+ ## False Positives
38
+ - Return value is checked with `require(success)` or equivalent
39
+ - The call is intentionally fire-and-forget (documented, no state depends on success) — rare but valid
40
+ - Using Solidity's high-level function calls (e.g., `IERC20(token).transfer(...)`) which auto-revert on failure
41
+
42
+ ## Remediation
43
+ - Always check the return value: `require(success, "call failed")`
44
+ - For ETH transfers to untrusted recipients, consider a pull-payment pattern to avoid DoS if the recipient deliberately reverts
45
+ - Prefer high-level Solidity calls when interacting with known interfaces
46
+ ```solidity
47
+ function withdraw(uint256 amount) external {
48
+ balances[msg.sender] -= amount;
49
+ (bool success,) = msg.sender.call{value: amount}("");
50
+ require(success, "ETH transfer failed");
51
+ }
52
+ ```
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: unencrypted-private-data-on-chain
3
+ description: - Sensitive data (passwords, secrets, private keys, game answers) is stored in contract storage
4
+ ---
5
+ <!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
6
+
7
+ # Unencrypted Private Data On-Chain
8
+
9
+ ## Preconditions
10
+ - Sensitive data (passwords, secrets, private keys, game answers) is stored in contract storage
11
+ - The developer relies on the `private` visibility modifier for confidentiality
12
+ - OR: sensitive data is passed as transaction calldata (publicly visible)
13
+
14
+ ## Vulnerable Pattern
15
+ ```solidity
16
+ contract SecretGame {
17
+ // `private` only prevents OTHER CONTRACTS from reading
18
+ // Anyone can read this via eth_getStorageAt(address, slot)
19
+ bytes32 private secretAnswer;
20
+ string private password;
21
+
22
+ constructor(bytes32 _answer, string memory _pwd) {
23
+ secretAnswer = _answer; // Visible in deployment tx calldata
24
+ password = _pwd; // Readable from storage slot
25
+ }
26
+
27
+ function guess(bytes32 _guess) external {
28
+ // Attacker reads secretAnswer from storage first
29
+ require(_guess == secretAnswer, "wrong");
30
+ _reward(msg.sender);
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Detection Heuristics
36
+ 1. Search for state variables storing passwords, secrets, keys, answers, or seeds — regardless of visibility modifier
37
+ 2. Check if any `private` variable is relied upon for confidentiality (not just access control)
38
+ 3. Look for game/lottery logic where hidden information is stored on-chain before a reveal phase
39
+ 4. Check constructor parameters and transaction calldata for sensitive values — these are publicly visible on block explorers
40
+ 5. Search for comments like "secret", "hidden", "private key", "password" near storage declarations
41
+
42
+ ## False Positives
43
+ - Data is encrypted or hashed before storage (e.g., commitment hash in a commit-reveal scheme)
44
+ - The `private` modifier is used correctly for access control between contracts, not for data confidentiality
45
+ - The "sensitive" data is actually public information (e.g., a contract address)
46
+
47
+ ## Remediation
48
+ - Never store plaintext secrets on-chain — all storage is publicly readable
49
+ - Use commit-reveal schemes for hidden inputs: store `keccak256(secret || salt)` first, reveal later
50
+ - For truly private data, keep it off-chain and only store hashes/commitments on-chain
51
+ - Consider zero-knowledge proofs for verifiable computation on private data
52
+ ```solidity
53
+ // Commit-reveal scheme
54
+ mapping(address => bytes32) public commitments;
55
+
56
+ function commit(bytes32 hash) external {
57
+ // User submits keccak256(answer, salt) — answer stays private
58
+ commitments[msg.sender] = hash;
59
+ }
60
+
61
+ function reveal(bytes32 answer, bytes32 salt) external {
62
+ require(commitments[msg.sender] == keccak256(abi.encodePacked(answer, salt)));
63
+ _processAnswer(msg.sender, answer);
64
+ }
65
+ ```
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: unexpected-ecrecover-null-address
3
+ description: - Contract uses `ecrecover` directly (not via OpenZeppelin's ECDSA library)
4
+ ---
5
+ <!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
6
+
7
+ # Unexpected ecrecover Null Address
8
+
9
+ ## Preconditions
10
+ - Contract uses `ecrecover` directly (not via OpenZeppelin's ECDSA library)
11
+ - The recovered address is not checked against `address(0)`
12
+ - The expected signer variable could be uninitialized (defaults to `address(0)`)
13
+
14
+ ## Vulnerable Pattern
15
+ ```solidity
16
+ contract Vault {
17
+ address public signer; // Uninitialized — defaults to address(0)
18
+
19
+ function withdrawWithSig(uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
20
+ bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount));
21
+
22
+ // ecrecover returns address(0) for invalid signatures
23
+ // (e.g., v != 27 && v != 28)
24
+ address recovered = ecrecover(hash, v, r, s);
25
+
26
+ // If signer is uninitialized (address(0)) and recovered is address(0),
27
+ // this check passes — anyone can withdraw
28
+ require(recovered == signer, "invalid signature");
29
+
30
+ _withdraw(msg.sender, amount);
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Detection Heuristics
36
+ 1. Search for `ecrecover(` calls in the codebase
37
+ 2. Check if the returned address is validated against `address(0)` — flag if not
38
+ 3. Check the variable that the recovered address is compared against — can it ever be `address(0)`? (uninitialized, never set, cleared by admin)
39
+ 4. Check if OpenZeppelin's `ECDSA.recover` is used instead — it reverts on null recovery automatically
40
+ 5. In upgradeable contracts, check if the signer is set during `initialize()` — if `initialize` is never called, signer remains `address(0)`
41
+
42
+ ## False Positives
43
+ - `require(recovered != address(0))` check is present after `ecrecover`
44
+ - OpenZeppelin's `ECDSA.recover` is used (handles null address internally)
45
+ - The expected signer is set in the constructor or initializer and can never be `address(0)` (validated on set)
46
+
47
+ ## Remediation
48
+ - Always check `require(recovered != address(0), "invalid signature")` after `ecrecover`
49
+ - Use OpenZeppelin's `ECDSA.recover` which reverts on invalid signatures and null recovery
50
+ - Validate that signer variables cannot be `address(0)` at any point
51
+ ```solidity
52
+ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
53
+
54
+ function withdrawWithSig(uint256 amount, bytes memory sig) external {
55
+ bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount));
56
+ bytes32 ethHash = ECDSA.toEthSignedMessageHash(hash);
57
+ address recovered = ECDSA.recover(ethHash, sig); // Reverts if address(0)
58
+ require(recovered == signer, "invalid signature");
59
+ _withdraw(msg.sender, amount);
60
+ }
61
+ ```
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: uninitialized-storage-pointer
3
+ description: - Solidity version <0.5.0
4
+ ---
5
+ <!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
6
+
7
+ # Uninitialized Storage Pointer
8
+
9
+ ## Preconditions
10
+ - Solidity version <0.5.0
11
+ - Local variables of complex types (structs, arrays) are declared without an explicit `storage` or `memory` data location
12
+ - The variable defaults to `storage`, pointing to slot 0
13
+
14
+ ## Vulnerable Pattern
15
+ ```solidity
16
+ // Solidity <0.5.0 only
17
+ pragma solidity ^0.4.24;
18
+
19
+ contract Registry {
20
+ address public owner; // Stored in slot 0
21
+ uint256 public totalUsers; // Stored in slot 1
22
+
23
+ struct User {
24
+ address addr;
25
+ uint256 balance;
26
+ }
27
+
28
+ User[] public users;
29
+
30
+ function addUser(address _addr, uint256 _balance) external {
31
+ // No data location specified — defaults to storage
32
+ // Points to slot 0 (owner) and slot 1 (totalUsers)
33
+ User u; // u.addr aliases slot 0 (owner)
34
+ u.addr = _addr; // Overwrites owner!
35
+ u.balance = _balance; // Overwrites totalUsers!
36
+ users.push(u);
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## Detection Heuristics
42
+ 1. Check the Solidity version: if >=0.5.0, this vulnerability is impossible (compiler requires explicit data location)
43
+ 2. For <0.5.0: search for local struct or array variable declarations without `storage` or `memory` keyword
44
+ 3. If a local variable of a complex type has no data location, it defaults to `storage` at slot 0 — writes to it overwrite the first state variables
45
+ 4. Check which state variables occupy slots 0, 1, 2, etc. — these are the ones at risk of overwrite
46
+ 5. Look for struct field assignments on local variables that could alias storage
47
+
48
+ ## False Positives
49
+ - Solidity >=0.5.0 (compiler enforces explicit data location — this can't happen)
50
+ - The variable is explicitly declared as `memory` (e.g., `User memory u`)
51
+ - The variable is explicitly declared as `storage` and intentionally points to a known storage location
52
+
53
+ ## Remediation
54
+ - Upgrade to Solidity >=0.5.0 where explicit data locations are compiler-enforced
55
+ - For legacy contracts, add explicit `memory` or `storage` to all local complex-type declarations
56
+ ```solidity
57
+ function addUser(address _addr, uint256 _balance) external {
58
+ User memory u; // Explicit memory — no storage aliasing
59
+ u.addr = _addr;
60
+ u.balance = _balance;
61
+ users.push(u);
62
+ }
63
+ ```
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: unsafe-low-level-call
3
+ description: - Contract uses `.call()`, `.delegatecall()`, `.staticcall()`, or `.send()` for external interactions
4
+ ---
5
+ <!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
6
+
7
+ # Unsafe Low-Level Call
8
+
9
+ ## Preconditions
10
+ - Contract uses `.call()`, `.delegatecall()`, `.staticcall()`, or `.send()` for external interactions
11
+ - The return value is not checked, OR
12
+ - The target address may not have deployed code (user-provided, destroyed, or never deployed)
13
+
14
+ ## Vulnerable Pattern
15
+ ```solidity
16
+ function payout(address to, uint256 amount) external {
17
+ // Unchecked return value — silent failure
18
+ to.call{value: amount}("");
19
+ totalPaid += amount; // Updated even if call failed
20
+ }
21
+
22
+ function interact(address target, bytes calldata data) external {
23
+ // Call to non-existent contract "succeeds" silently
24
+ // EVM treats call to codeless address as successful
25
+ (bool success,) = target.call(data);
26
+ require(success); // Passes even if target has no code!
27
+ _markComplete();
28
+ }
29
+ ```
30
+
31
+ ## Detection Heuristics
32
+ 1. Search for `.call(`, `.send(`, `.delegatecall(`, `.staticcall(` in the codebase
33
+ 2. For each, check if the returned `bool` is captured AND checked (e.g., `require(success)`)
34
+ 3. If the return value is not captured or not checked, flag it — execution continues after failure
35
+ 4. For calls to user-supplied addresses, check if `target.code.length > 0` is verified before the call — the EVM silently succeeds on calls to addresses with no code
36
+ 5. Note: `address.code.length` check can be bypassed during constructor execution (code size is 0)
37
+ 6. Check if state changes after the call assume it succeeded
38
+
39
+ ## False Positives
40
+ - Return value is properly checked with `require(success)`
41
+ - High-level Solidity calls are used (e.g., `IERC20(token).transfer(...)`) which include automatic `extcodesize` checks and revert on failure
42
+ - The call is intentionally fire-and-forget with no state depending on success (rare, must be documented)
43
+
44
+ ## Remediation
45
+ - Always check return values: `require(success, "call failed")`
46
+ - Verify target has code before low-level calls: `require(target.code.length > 0)`
47
+ - Prefer high-level Solidity calls for known interfaces — they include automatic code existence checks
48
+ - For critical integrations, combine both checks
49
+ ```solidity
50
+ function payout(address to, uint256 amount) external {
51
+ require(to.code.length > 0 || to == tx.origin, "no code at target");
52
+ (bool success,) = to.call{value: amount}("");
53
+ require(success, "transfer failed");
54
+ totalPaid += amount;
55
+ }
56
+ ```
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: unsecure-signatures
3
+ description: - Contract uses ECDSA signatures for authorization, authentication, or message verification
4
+ ---
5
+ <!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
6
+
7
+ # Unsecure Signatures
8
+
9
+ ## Preconditions
10
+ - Contract uses ECDSA signatures for authorization, authentication, or message verification
11
+ - One or more of the following sub-vulnerabilities are present:
12
+ - Signature malleability (tracking by raw bytes)
13
+ - Missing replay protection (no nonce, chainId, or contract address)
14
+ - Unchecked ecrecover null address return
15
+ - Hash collisions from `abi.encodePacked` with multiple dynamic types
16
+ - No EIP-712 structured data signing
17
+
18
+ ## Vulnerable Pattern
19
+ ```solidity
20
+ // Combines multiple signature anti-patterns
21
+ function execute(bytes memory sig, address to, uint256 amount) external {
22
+ // 1. No nonce — replay attack
23
+ // 2. No address(this) — cross-contract replay
24
+ // 3. No block.chainid — cross-chain replay
25
+ bytes32 hash = keccak256(abi.encodePacked(to, amount));
26
+
27
+ // 4. Raw ecrecover — no null address check
28
+ address recovered = ecrecover(hash, v, r, s);
29
+ // 5. No s-value malleability check
30
+ require(recovered == signer);
31
+
32
+ // 6. Signature tracked by bytes — malleable bypass
33
+ require(!used[sig]);
34
+ used[sig] = true;
35
+
36
+ _transfer(to, amount);
37
+ }
38
+ ```
39
+
40
+ ## Detection Heuristics
41
+ 1. Search for `ecrecover` or `ECDSA.recover` — this indicates signature usage
42
+ 2. Check each sub-vulnerability in order:
43
+ - **Malleability**: is deduplication done by raw signature bytes? Flag if yes
44
+ - **Replay**: does the signed hash include nonce + `address(this)` + `block.chainid`? Flag any missing
45
+ - **Null address**: is the recovered address checked against `address(0)`? Flag if not
46
+ - **Hash collision**: is `abi.encodePacked` used with multiple dynamic types? Flag if yes
47
+ - **EIP-712**: is structured typed data signing used? Flag if not (lower severity)
48
+ 3. Check if OpenZeppelin's ECDSA library is used — it handles malleability and null address automatically
49
+ 4. Check for front-running risk: can a signed message be observed in the mempool and submitted by someone else?
50
+
51
+ ## False Positives
52
+ - OpenZeppelin's ECDSA library is used with EIP-712 domain separator, nonce tracking, and proper hash construction — all sub-vulnerabilities are addressed
53
+ - EIP-2612 permit pattern is used correctly (includes nonce, deadline, domain separator)
54
+ - The contract is a simple forwarder where signature security is handled by a downstream contract
55
+
56
+ ## Remediation
57
+ - Use OpenZeppelin's ECDSA library for signature recovery (handles malleability + null address)
58
+ - Implement EIP-712 structured data signing with domain separator (covers chainId + contract address)
59
+ - Track nonces per signer to prevent replay
60
+ - Use `abi.encode` instead of `abi.encodePacked` for hash construction
61
+ ```solidity
62
+ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
63
+ import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
64
+
65
+ contract SecureSig is EIP712("SecureSig", "1") {
66
+ mapping(address => uint256) public nonces;
67
+
68
+ bytes32 constant EXECUTE_TYPEHASH =
69
+ keccak256("Execute(address to,uint256 amount,uint256 nonce)");
70
+
71
+ function execute(address to, uint256 amount, bytes memory sig) external {
72
+ uint256 nonce = nonces[msg.sender]++;
73
+ bytes32 structHash = keccak256(abi.encode(EXECUTE_TYPEHASH, to, amount, nonce));
74
+ bytes32 hash = _hashTypedDataV4(structHash);
75
+ address recovered = ECDSA.recover(hash, sig);
76
+ require(recovered == signer, "invalid sig");
77
+ _transfer(to, amount);
78
+ }
79
+ }
80
+ ```
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: unsupported-opcodes
3
+ description: - Contract is intended for deployment on an EVM-compatible chain other than Ethereum mainnet (zkSync Era, Arbitrum, Optimism, Polygon, BNB Chain, etc.)
4
+ ---
5
+ <!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
6
+
7
+ # Unsupported Opcodes on EVM-Compatible Chains
8
+
9
+ ## Preconditions
10
+ - Contract is intended for deployment on an EVM-compatible chain other than Ethereum mainnet (zkSync Era, Arbitrum, Optimism, Polygon, BNB Chain, etc.)
11
+ - The contract uses opcodes or patterns that are not supported or behave differently on the target chain
12
+
13
+ ## Vulnerable Pattern
14
+ ```solidity
15
+ // PUSH0 opcode: Solidity >=0.8.20 emits PUSH0
16
+ // Not supported on all chains — contract fails to deploy
17
+ pragma solidity 0.8.20;
18
+
19
+ contract Token {
20
+ // Compiled bytecode contains PUSH0 — reverts on chains without support
21
+ }
22
+
23
+ // .transfer() on zkSync Era — 2300 gas stipend insufficient
24
+ function withdraw() external {
25
+ // transfer() forwards only 2300 gas
26
+ // On zkSync Era, basic operations cost more — this always reverts
27
+ // Gemholic lost 921 ETH to this exact issue
28
+ payable(msg.sender).transfer(amount);
29
+ }
30
+
31
+ // Dynamic create on zkSync Era
32
+ function deploy(bytes memory bytecode) external {
33
+ assembly {
34
+ // zkSync requires bytecode known at compile time
35
+ // Runtime create from arbitrary bytecode fails
36
+ let addr := create(0, add(bytecode, 0x20), mload(bytecode))
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## Detection Heuristics
42
+ 1. Check the Solidity version: if >=0.8.20, verify the target chain supports the PUSH0 opcode
43
+ 2. Search for `.transfer()` and `.send()` — these use a 2300 gas stipend that may be insufficient on chains with different gas cost structures (especially zkSync Era)
44
+ 3. Search for assembly `create` / `create2` with runtime-supplied bytecode — this fails on zkSync Era
45
+ 4. Search for `selfdestruct` — deprecated and non-functional on some chains post-Dencun
46
+ 5. If the project targets multiple chains, cross-reference all opcodes used against each target chain's compatibility (use evmdiff.com)
47
+
48
+ ## False Positives
49
+ - Contract is only deployed on Ethereum mainnet
50
+ - Solidity version is <0.8.20 (no PUSH0 emitted)
51
+ - `.call{value: amount}("")` is used instead of `.transfer()` (forwards all available gas)
52
+ - The project explicitly documents which chains are supported and tests against them
53
+
54
+ ## Remediation
55
+ - Use `.call{value: amount}("")` instead of `.transfer()` or `.send()` for ETH transfers
56
+ - For multi-chain deployments, compile with Solidity <0.8.20 or use `--evm-version paris` to avoid PUSH0
57
+ - On zkSync Era, use compile-time known bytecode for contract creation
58
+ - Test deployments on each target chain before mainnet launch
59
+ - Use evmdiff.com to verify opcode compatibility per chain
60
+ ```solidity
61
+ // Safe ETH transfer for all EVM chains
62
+ function withdraw(uint256 amount) external {
63
+ (bool success,) = msg.sender.call{value: amount}("");
64
+ require(success, "transfer failed");
65
+ }
66
+
67
+ // Avoid PUSH0: compile with Paris EVM target
68
+ // solc --evm-version paris
69
+ ```