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.
- package/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/package.json +43 -0
- package/skills/INVENTORY.md +79 -0
- package/skills/README.md +56 -0
- package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +424 -0
- package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +157 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +373 -0
- package/skills/checklists/cyfrin-defi-integrations/SKILL.md +412 -0
- package/skills/checklists/cyfrin-gas/SKILL.md +55 -0
- package/skills/checklists/general-audit/SKILL.md +433 -0
- package/skills/methodology/audit-workflow/SKILL.md +129 -0
- package/skills/methodology/report-template/SKILL.md +190 -0
- package/skills/methodology/severity-classification/SKILL.md +179 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +229 -0
- package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +317 -0
- package/skills/protocol-patterns/dao-governance/SKILL.md +281 -0
- package/skills/protocol-patterns/lending-borrowing/SKILL.md +221 -0
- package/skills/protocol-patterns/staking-vesting/SKILL.md +247 -0
- package/skills/references/exploit-reference/SKILL.md +259 -0
- package/skills/references/smartbugs-examples/SKILL.md +296 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +298 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +59 -0
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +59 -0
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +61 -0
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +55 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +62 -0
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +60 -0
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +59 -0
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +72 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +249 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +51 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +52 -0
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +61 -0
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +60 -0
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +59 -0
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +61 -0
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +61 -0
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +333 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +60 -0
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +66 -0
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +67 -0
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +252 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +65 -0
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +61 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +266 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +72 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +59 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +63 -0
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +52 -0
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +65 -0
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +61 -0
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +63 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +56 -0
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +80 -0
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +69 -0
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +70 -0
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +81 -0
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +77 -0
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +294 -0
- package/src/agents/argus-prompt.ts +407 -0
- package/src/agents/pythia-prompt.ts +134 -0
- package/src/agents/scribe-prompt.ts +87 -0
- package/src/agents/sentinel-prompt.ts +133 -0
- package/src/cli/cli-program.ts +67 -0
- package/src/cli/commands/doctor.ts +83 -0
- package/src/cli/commands/init.ts +46 -0
- package/src/cli/commands/install.ts +55 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/tui-prompts.ts +75 -0
- package/src/cli/types.ts +9 -0
- package/src/config/index.ts +3 -0
- package/src/config/loader.ts +36 -0
- package/src/config/schema.ts +82 -0
- package/src/config/types.ts +4 -0
- package/src/constants/defaults.ts +6 -0
- package/src/create-hooks.ts +84 -0
- package/src/create-managers.ts +26 -0
- package/src/create-tools.ts +30 -0
- package/src/features/audit-enforcer/audit-enforcer.ts +34 -0
- package/src/features/audit-enforcer/index.ts +1 -0
- package/src/features/background-agent/background-manager.ts +200 -0
- package/src/features/background-agent/index.ts +1 -0
- package/src/features/context-monitor/context-monitor.ts +48 -0
- package/src/features/context-monitor/index.ts +4 -0
- package/src/features/context-monitor/tool-output-truncator.ts +17 -0
- package/src/features/error-recovery/index.ts +2 -0
- package/src/features/error-recovery/session-recovery.ts +27 -0
- package/src/features/error-recovery/tool-error-recovery.ts +35 -0
- package/src/features/index.ts +5 -0
- package/src/features/persistent-state/audit-state-manager.ts +121 -0
- package/src/features/persistent-state/index.ts +1 -0
- package/src/hooks/compaction-hook.ts +50 -0
- package/src/hooks/config-handler.ts +116 -0
- package/src/hooks/event-hook-v2.ts +93 -0
- package/src/hooks/event-hook.ts +74 -0
- package/src/hooks/hook-system.ts +9 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/knowledge-sync-hook.ts +57 -0
- package/src/hooks/safe-create-hook.ts +15 -0
- package/src/hooks/system-prompt-hook.ts +126 -0
- package/src/hooks/tool-tracking-hook.ts +234 -0
- package/src/hooks/types.ts +16 -0
- package/src/index.ts +36 -0
- package/src/knowledge/scvd-client.ts +242 -0
- package/src/knowledge/scvd-index.ts +183 -0
- package/src/knowledge/scvd-sync.ts +85 -0
- package/src/managers/index.ts +1 -0
- package/src/managers/types.ts +85 -0
- package/src/plugin-interface.ts +38 -0
- package/src/shared/binary-utils.ts +63 -0
- package/src/shared/deep-merge.ts +71 -0
- package/src/shared/file-utils.ts +56 -0
- package/src/shared/index.ts +5 -0
- package/src/shared/jsonc-parser.ts +39 -0
- package/src/shared/logger.ts +36 -0
- package/src/state/audit-state.ts +27 -0
- package/src/state/finding-store.ts +126 -0
- package/src/state/plugin-state.ts +14 -0
- package/src/state/types.ts +61 -0
- package/src/tools/contract-analyzer-tool.ts +184 -0
- package/src/tools/forge-fuzz-tool.ts +311 -0
- package/src/tools/forge-test-tool.ts +397 -0
- package/src/tools/pattern-checker-tool.ts +337 -0
- package/src/tools/report-generator-tool.ts +308 -0
- package/src/tools/slither-tool.ts +465 -0
- package/src/tools/solodit-search-tool.ts +131 -0
- package/src/tools/sync-knowledge-tool.ts +116 -0
- package/src/utils/project-detector.ts +133 -0
- 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
|
+
```
|