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,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: insufficient-gas-griefing
|
|
3
|
+
description: - Contract relays or forwards calls on behalf of users (meta-transactions, multisig execution, relayer patterns)
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Insufficient Gas Griefing
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract relays or forwards calls on behalf of users (meta-transactions, multisig execution, relayer patterns)
|
|
11
|
+
- The relayer/executor controls how much gas is forwarded to the sub-call
|
|
12
|
+
- Replay protection (nonce/hash marking) occurs before or regardless of sub-call success
|
|
13
|
+
- No minimum gas requirement is enforced before the sub-call
|
|
14
|
+
|
|
15
|
+
## Vulnerable Pattern
|
|
16
|
+
```solidity
|
|
17
|
+
function execute(address target, bytes calldata data, uint256 gasLimit) external {
|
|
18
|
+
// Replay protection BEFORE sub-call — marks as executed regardless
|
|
19
|
+
require(!executed[nonce], "already executed");
|
|
20
|
+
executed[nonce] = true;
|
|
21
|
+
nonce++;
|
|
22
|
+
|
|
23
|
+
// Relayer can provide just enough gas for the outer tx to succeed
|
|
24
|
+
// but insufficient gas for the inner call — it silently fails
|
|
25
|
+
(bool success,) = target.call{gas: gasLimit}(data);
|
|
26
|
+
// success is false, but the nonce is already consumed
|
|
27
|
+
// The action is permanently censored
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Detection Heuristics
|
|
32
|
+
1. Identify relayer/meta-transaction patterns: functions that execute calls on behalf of other users
|
|
33
|
+
2. Check if the function marks a nonce/hash as used BEFORE the sub-call succeeds — this enables permanent censorship
|
|
34
|
+
3. Check if there's a `gasleft()` validation before the sub-call (e.g., `require(gasleft() >= requiredGas + overhead)`)
|
|
35
|
+
4. Look for multisig `execute` functions where the executor controls gas forwarding
|
|
36
|
+
5. Check if `.call{gas: X}` is used where `X` comes from the caller — the caller can set it too low
|
|
37
|
+
|
|
38
|
+
## False Positives
|
|
39
|
+
- Replay protection only marks the nonce after confirming sub-call success
|
|
40
|
+
- The function enforces a minimum gas requirement before the sub-call
|
|
41
|
+
- The sub-call failure is propagated (e.g., `require(success)`) so the outer tx also reverts, preserving the nonce
|
|
42
|
+
- The gas parameter is fixed or validated against a minimum
|
|
43
|
+
|
|
44
|
+
## Remediation
|
|
45
|
+
- Enforce minimum gas before sub-calls: `require(gasleft() >= gasLimit + OVERHEAD)`
|
|
46
|
+
- Only mark nonces/hashes as used AFTER confirming sub-call success
|
|
47
|
+
- Propagate sub-call failures to revert the outer transaction when appropriate
|
|
48
|
+
- Use EIP-150 rule awareness: the caller retains 1/64 of gas, so forward at least `gasLimit * 64/63`
|
|
49
|
+
```solidity
|
|
50
|
+
function execute(address target, bytes calldata data, uint256 gasLimit) external {
|
|
51
|
+
require(gasleft() >= gasLimit + 10000, "insufficient gas");
|
|
52
|
+
|
|
53
|
+
(bool success, bytes memory result) = target.call{gas: gasLimit}(data);
|
|
54
|
+
|
|
55
|
+
// Only mark as executed if sub-call succeeded
|
|
56
|
+
if (success) {
|
|
57
|
+
executed[nonce] = true;
|
|
58
|
+
nonce++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lack-of-precision
|
|
3
|
+
description: - Contract performs integer arithmetic (division, fee calculations, reward distributions)
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Lack of Precision
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract performs integer arithmetic (division, fee calculations, reward distributions)
|
|
11
|
+
- Division is performed before multiplication, OR
|
|
12
|
+
- Numerators can be smaller than denominators (producing zero), OR
|
|
13
|
+
- No fixed-point scaling (WAD/RAY) is used for fractional calculations
|
|
14
|
+
|
|
15
|
+
## Vulnerable Pattern
|
|
16
|
+
```solidity
|
|
17
|
+
function calculateFee(uint256 amount, uint256 daysEarly) external view returns (uint256) {
|
|
18
|
+
// Division BEFORE multiplication — truncates intermediate result
|
|
19
|
+
uint256 dailyRate = amount / 365; // Loses precision
|
|
20
|
+
uint256 fee = dailyRate * daysEarly; // Error compounds
|
|
21
|
+
|
|
22
|
+
// Correct: amount * daysEarly / 365 (multiply first)
|
|
23
|
+
return fee;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function distribute(uint256 reward, uint256 totalShares) external {
|
|
27
|
+
for (uint256 i = 0; i < holders.length; i++) {
|
|
28
|
+
// If reward < totalShares, this is always 0
|
|
29
|
+
uint256 share = reward / totalShares * balances[holders[i]];
|
|
30
|
+
_transfer(holders[i], share);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Detection Heuristics
|
|
36
|
+
1. Search for division operations (`/`) in arithmetic expressions
|
|
37
|
+
2. Check if division appears before multiplication in the same expression — this loses precision
|
|
38
|
+
3. Check if the numerator can be smaller than the denominator — the result truncates to zero
|
|
39
|
+
4. Look for fee, reward, interest, or share calculations without scaling factors (e.g., 1e18)
|
|
40
|
+
5. Check rounding direction: does truncation favor the protocol or the user? In fee/debt calculations, rounding should favor the protocol; in reward/credit calculations, it should favor the user
|
|
41
|
+
|
|
42
|
+
## False Positives
|
|
43
|
+
- Multiplication is performed before division in the correct order
|
|
44
|
+
- Fixed-point math libraries (WAD = 1e18, RAY = 1e27) are used to maintain precision
|
|
45
|
+
- The numerator is guaranteed to be larger than the denominator by prior validation
|
|
46
|
+
- The precision loss is intentionally accepted and documented (e.g., dust amounts)
|
|
47
|
+
|
|
48
|
+
## Remediation
|
|
49
|
+
- Always multiply before dividing: `amount * rate / divisor` instead of `amount / divisor * rate`
|
|
50
|
+
- Use fixed-point math with scaling factors (1e18 for WAD, 1e27 for RAY)
|
|
51
|
+
- Round in favor of the protocol for fees/debts, in favor of users for rewards/credits
|
|
52
|
+
- Use `mulDiv` from OpenZeppelin or PRBMath for safe full-precision multiplication then division
|
|
53
|
+
```solidity
|
|
54
|
+
// Correct: multiply first, then divide
|
|
55
|
+
uint256 fee = amount * daysEarly / 365;
|
|
56
|
+
|
|
57
|
+
// With scaling for precision
|
|
58
|
+
uint256 WAD = 1e18;
|
|
59
|
+
uint256 scaledRate = (amount * WAD) / totalSupply;
|
|
60
|
+
uint256 reward = (scaledRate * userBalance) / WAD;
|
|
61
|
+
```
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: logic-errors
|
|
3
|
+
description: Protocol logic bug patterns, exploit examples, and invariant-driven review strategies.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Source: DeFiFoFum/fofum-solidity-skills (MIT) -->
|
|
7
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
8
|
+
|
|
9
|
+
# Logic Bug Exploits
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Logic bugs are flaws in business logic that allow unintended behavior. Unlike pattern-based vulnerabilities (reentrancy, overflow), these require understanding the protocol's intended behavior to identify.
|
|
14
|
+
|
|
15
|
+
**Key insight:** Logic bugs are the hardest to find automatically and cause the largest losses.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Attack Patterns
|
|
20
|
+
|
|
21
|
+
### 1. Incorrect State Machine
|
|
22
|
+
|
|
23
|
+
```solidity
|
|
24
|
+
// VULNERABLE: Can withdraw multiple times
|
|
25
|
+
enum Status { Pending, Approved, Withdrawn }
|
|
26
|
+
mapping(uint256 => Status) public requestStatus;
|
|
27
|
+
|
|
28
|
+
function withdraw(uint256 requestId) external {
|
|
29
|
+
require(requestStatus[requestId] == Status.Approved, "Not approved");
|
|
30
|
+
// Missing: requestStatus[requestId] = Status.Withdrawn;
|
|
31
|
+
payable(msg.sender).transfer(amounts[requestId]);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Order of Operations
|
|
36
|
+
|
|
37
|
+
```solidity
|
|
38
|
+
// VULNERABLE: Update after external call
|
|
39
|
+
function redeem(uint256 shares) external {
|
|
40
|
+
uint256 assets = convertToAssets(shares);
|
|
41
|
+
asset.transfer(msg.sender, assets); // External call first
|
|
42
|
+
_burn(msg.sender, shares); // State update after
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// SECURE: CEI pattern
|
|
46
|
+
function redeem(uint256 shares) external {
|
|
47
|
+
uint256 assets = convertToAssets(shares);
|
|
48
|
+
_burn(msg.sender, shares); // State update first
|
|
49
|
+
asset.transfer(msg.sender, assets); // External call after
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3. Accounting Errors
|
|
54
|
+
|
|
55
|
+
```solidity
|
|
56
|
+
// VULNERABLE: Doesn't account for existing balance
|
|
57
|
+
function deposit() external payable {
|
|
58
|
+
balances[msg.sender] = msg.value; // Overwrites, doesn't add!
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// SECURE
|
|
62
|
+
function deposit() external payable {
|
|
63
|
+
balances[msg.sender] += msg.value;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 4. Precision Loss
|
|
68
|
+
|
|
69
|
+
```solidity
|
|
70
|
+
// VULNERABLE: Division before multiplication
|
|
71
|
+
function calculateReward(uint256 amount, uint256 rate) external pure returns (uint256) {
|
|
72
|
+
return amount / 1000 * rate; // Loses precision
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// SECURE: Multiplication before division
|
|
76
|
+
function calculateReward(uint256 amount, uint256 rate) external pure returns (uint256) {
|
|
77
|
+
return amount * rate / 1000;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 5. Edge Case Handling
|
|
82
|
+
|
|
83
|
+
```solidity
|
|
84
|
+
// VULNERABLE: Division by zero when totalSupply is 0
|
|
85
|
+
function pricePerShare() external view returns (uint256) {
|
|
86
|
+
return totalAssets() / totalSupply(); // Reverts if no shares
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// SECURE
|
|
90
|
+
function pricePerShare() external view returns (uint256) {
|
|
91
|
+
uint256 supply = totalSupply();
|
|
92
|
+
return supply == 0 ? 1e18 : totalAssets() * 1e18 / supply;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Real Exploits
|
|
99
|
+
|
|
100
|
+
### Nomad Bridge (Aug 2022) — $190M
|
|
101
|
+
|
|
102
|
+
**What happened:**
|
|
103
|
+
- Routine upgrade initialized `confirmAt` mapping with zero
|
|
104
|
+
- Zero was a valid "confirmed" state
|
|
105
|
+
- Anyone could submit fake messages as "already proven"
|
|
106
|
+
- Mass exploitation by hundreds of addresses
|
|
107
|
+
|
|
108
|
+
**Root cause:** Invalid initialization treated as valid state
|
|
109
|
+
|
|
110
|
+
```solidity
|
|
111
|
+
// The bug: messages[_messageHash] = 0 was treated as confirmed
|
|
112
|
+
function process(bytes memory _message) public returns (bool _success) {
|
|
113
|
+
bytes32 _messageHash = keccak256(_message);
|
|
114
|
+
require(acceptableRoot(messages[_messageHash]), "not accepted");
|
|
115
|
+
// 0 passed acceptableRoot check!
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Lesson:** Be explicit about valid states. Zero should not be a valid confirmed state.
|
|
120
|
+
|
|
121
|
+
### Compound (Sept 2021) — $80M+
|
|
122
|
+
|
|
123
|
+
**What happened:**
|
|
124
|
+
- Upgrade introduced bug in reward distribution
|
|
125
|
+
- `comptroller.compAccrued()` returned wrong values
|
|
126
|
+
- Users could claim excessive COMP rewards
|
|
127
|
+
|
|
128
|
+
**Root cause:** Incorrect accounting in upgrade
|
|
129
|
+
|
|
130
|
+
**Lesson:** Test upgrades extensively. State transitions are dangerous.
|
|
131
|
+
|
|
132
|
+
### Level Finance (May 2023) — $1.1M
|
|
133
|
+
|
|
134
|
+
**What happened:**
|
|
135
|
+
- Referral reward calculation bug
|
|
136
|
+
- Could claim referral rewards multiple times
|
|
137
|
+
- Self-referral exploit
|
|
138
|
+
|
|
139
|
+
**Root cause:** Missing state update after reward claim
|
|
140
|
+
|
|
141
|
+
### Saddle Finance (Apr 2022) — $10M
|
|
142
|
+
|
|
143
|
+
**What happened:**
|
|
144
|
+
- Virtual price calculation bug in meta-pool
|
|
145
|
+
- Attacker manipulated LP token value
|
|
146
|
+
- Withdrew more than deposited
|
|
147
|
+
|
|
148
|
+
**Root cause:** Incorrect share calculation with rounding
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Detection Checklist
|
|
153
|
+
|
|
154
|
+
### State Machine
|
|
155
|
+
- [ ] Are all state transitions explicitly handled?
|
|
156
|
+
- [ ] Can states be skipped or repeated?
|
|
157
|
+
- [ ] Is there a state that represents "completed" to prevent re-execution?
|
|
158
|
+
- [ ] Are default/zero values handled as invalid states?
|
|
159
|
+
|
|
160
|
+
### Accounting
|
|
161
|
+
- [ ] Does deposit add to existing balance (not overwrite)?
|
|
162
|
+
- [ ] Does withdrawal subtract correctly?
|
|
163
|
+
- [ ] Are shares/assets conversions precise?
|
|
164
|
+
- [ ] Is totalSupply/totalAssets always consistent?
|
|
165
|
+
|
|
166
|
+
### Math & Precision
|
|
167
|
+
- [ ] Is multiplication done before division?
|
|
168
|
+
- [ ] Are there division-by-zero possibilities?
|
|
169
|
+
- [ ] Is precision loss handled (rounding direction)?
|
|
170
|
+
- [ ] Are there overflow possibilities in intermediate calculations?
|
|
171
|
+
|
|
172
|
+
### Edge Cases
|
|
173
|
+
- [ ] What happens with zero amounts?
|
|
174
|
+
- [ ] What happens with very small/large values?
|
|
175
|
+
- [ ] What happens on first deposit (empty pool)?
|
|
176
|
+
- [ ] What happens on last withdrawal (drain pool)?
|
|
177
|
+
- [ ] What happens with self-referral/self-liquidation?
|
|
178
|
+
|
|
179
|
+
### Protocol-Specific
|
|
180
|
+
- [ ] Can rewards be claimed multiple times?
|
|
181
|
+
- [ ] Can positions be liquidated incorrectly?
|
|
182
|
+
- [ ] Are fees calculated and deducted correctly?
|
|
183
|
+
- [ ] Can users bypass intended restrictions?
|
|
184
|
+
|
|
185
|
+
## Finding Logic Bugs
|
|
186
|
+
|
|
187
|
+
### 1. Understand Intended Behavior
|
|
188
|
+
- Read docs, comments, specs
|
|
189
|
+
- What SHOULD happen?
|
|
190
|
+
- What are the invariants?
|
|
191
|
+
|
|
192
|
+
### 2. Trace State Changes
|
|
193
|
+
- Follow every state variable modification
|
|
194
|
+
- Map out the state machine
|
|
195
|
+
- Find missing transitions
|
|
196
|
+
|
|
197
|
+
### 3. Test Boundaries
|
|
198
|
+
- Zero values
|
|
199
|
+
- Max values
|
|
200
|
+
- First/last operations
|
|
201
|
+
- Empty states
|
|
202
|
+
|
|
203
|
+
### 4. Question Assumptions
|
|
204
|
+
- "Users won't do X" — They will
|
|
205
|
+
- "This value can't be zero" — It can
|
|
206
|
+
- "This is called after Y" — Is it enforced?
|
|
207
|
+
|
|
208
|
+
### 5. Think Like an Attacker
|
|
209
|
+
- What's the most valuable exploit?
|
|
210
|
+
- What if I could control X?
|
|
211
|
+
- What if I did operations out of order?
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Secure Patterns
|
|
216
|
+
|
|
217
|
+
### Explicit State Machine
|
|
218
|
+
|
|
219
|
+
```solidity
|
|
220
|
+
enum State { Created, Active, Completed, Cancelled }
|
|
221
|
+
|
|
222
|
+
modifier inState(State expected) {
|
|
223
|
+
require(state == expected, "Invalid state");
|
|
224
|
+
_;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function complete() external inState(State.Active) {
|
|
228
|
+
state = State.Completed; // Explicit transition
|
|
229
|
+
// Do completion logic
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Invariant Checks
|
|
234
|
+
|
|
235
|
+
```solidity
|
|
236
|
+
function _checkInvariants() internal view {
|
|
237
|
+
assert(totalAssets >= totalLiabilities);
|
|
238
|
+
assert(totalShares == 0 || totalAssets > 0);
|
|
239
|
+
// Add protocol-specific invariants
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function deposit(uint256 assets) external {
|
|
243
|
+
// ... deposit logic ...
|
|
244
|
+
_checkInvariants();
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### CEI Pattern (Checks-Effects-Interactions)
|
|
249
|
+
|
|
250
|
+
```solidity
|
|
251
|
+
function withdraw(uint256 amount) external {
|
|
252
|
+
// CHECKS
|
|
253
|
+
require(balances[msg.sender] >= amount, "Insufficient");
|
|
254
|
+
|
|
255
|
+
// EFFECTS
|
|
256
|
+
balances[msg.sender] -= amount;
|
|
257
|
+
|
|
258
|
+
// INTERACTIONS
|
|
259
|
+
payable(msg.sender).transfer(amount);
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## References
|
|
266
|
+
|
|
267
|
+
- [Nomad Hack Analysis (samczsun)](https://www.paradigm.xyz/2022/08/how-to-drain-almost-1-billion)
|
|
268
|
+
- [Compound Bug Post-Mortem](https://www.comp.xyz/t/compound-comptroller-bug-post-mortem/2284)
|
|
269
|
+
- [Trail of Bits: Not So Smart Contracts](https://github.com/crytic/not-so-smart-contracts)
|
|
270
|
+
|
|
271
|
+
## Validation Heuristics (kadenzipfel)
|
|
272
|
+
|
|
273
|
+
## Preconditions
|
|
274
|
+
- Contract uses `require()` statements for input or state validation
|
|
275
|
+
- The `require` condition is overly restrictive (rejects valid inputs) or too loose (accepts invalid inputs)
|
|
276
|
+
- OR: `require()` validates return values from external contracts whose behavior doesn't match assumptions
|
|
277
|
+
|
|
278
|
+
## Vulnerable Pattern
|
|
279
|
+
```solidity
|
|
280
|
+
// Overly restrictive: blocks legitimate use case
|
|
281
|
+
function withdraw(uint256 amount) external {
|
|
282
|
+
// Fails if user has exactly the required amount (should be >=)
|
|
283
|
+
require(balances[msg.sender] > amount, "insufficient");
|
|
284
|
+
balances[msg.sender] -= amount;
|
|
285
|
+
payable(msg.sender).transfer(amount);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// External contract assumption mismatch
|
|
289
|
+
function processPayment(IERC20 token, uint256 amount) external {
|
|
290
|
+
// Assumes transfer returns true, but some tokens don't return a value
|
|
291
|
+
// require reverts on tokens like USDT
|
|
292
|
+
require(token.transfer(msg.sender, amount), "failed");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Missing error message
|
|
296
|
+
function setRate(uint256 rate) external {
|
|
297
|
+
require(rate > 0); // No error message — difficult to debug
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Detection Heuristics
|
|
302
|
+
1. For each `require()`, verify the condition matches the documented business logic (e.g., `>` vs `>=`, `<` vs `<=`)
|
|
303
|
+
2. Check if `require()` validates an external call's return value — verify the external contract actually returns what's expected
|
|
304
|
+
3. Look for `require()` without error messages — while not a vulnerability, it makes debugging difficult
|
|
305
|
+
4. Check if overly strict requirements can DoS critical paths (e.g., withdrawals, liquidations)
|
|
306
|
+
5. Check chained contract calls: does an upstream contract provide inputs that could fail downstream `require` checks?
|
|
307
|
+
|
|
308
|
+
## False Positives
|
|
309
|
+
- The `require` condition exactly matches the specification
|
|
310
|
+
- The strictness is intentional and documented
|
|
311
|
+
- The error message is omitted in a pre-0.8.4 contract for gas optimization (custom errors not yet available)
|
|
312
|
+
|
|
313
|
+
## Remediation
|
|
314
|
+
- Verify each `require` condition against the specification: `>` vs `>=`, `<` vs `<=`
|
|
315
|
+
- Add descriptive error messages or use custom errors (Solidity >=0.8.4)
|
|
316
|
+
- For external call validations, use SafeERC20 or similar wrappers that handle non-standard return values
|
|
317
|
+
```solidity
|
|
318
|
+
function withdraw(uint256 amount) external {
|
|
319
|
+
require(balances[msg.sender] >= amount, "insufficient balance");
|
|
320
|
+
balances[msg.sender] -= amount;
|
|
321
|
+
payable(msg.sender).transfer(amount);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Solidity >=0.8.4: custom errors for gas efficiency
|
|
325
|
+
error InsufficientBalance(uint256 available, uint256 requested);
|
|
326
|
+
|
|
327
|
+
function withdrawV2(uint256 amount) external {
|
|
328
|
+
if (balances[msg.sender] < amount)
|
|
329
|
+
revert InsufficientBalance(balances[msg.sender], amount);
|
|
330
|
+
balances[msg.sender] -= amount;
|
|
331
|
+
payable(msg.sender).transfer(amount);
|
|
332
|
+
}
|
|
333
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: missing-protection-signature-replay
|
|
3
|
+
description: - Contract verifies ECDSA signatures for authorization
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Missing Protection Against Signature Replay
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract verifies ECDSA signatures for authorization
|
|
11
|
+
- The signed message does not include a nonce, OR does not include the contract address, OR does not include the chain ID
|
|
12
|
+
- No mechanism tracks which signatures have been processed
|
|
13
|
+
|
|
14
|
+
## Vulnerable Pattern
|
|
15
|
+
```solidity
|
|
16
|
+
function executeWithSig(address to, uint256 amount, bytes memory sig) external {
|
|
17
|
+
// Missing: nonce, contract address, and chain ID in hash
|
|
18
|
+
// Same signature can be replayed on same contract, other contracts, or other chains
|
|
19
|
+
bytes32 hash = keccak256(abi.encodePacked(to, amount));
|
|
20
|
+
address signer = ECDSA.recover(hash, sig);
|
|
21
|
+
require(signer == authorizer, "invalid sig");
|
|
22
|
+
|
|
23
|
+
// No nonce tracking — same signature can be submitted repeatedly
|
|
24
|
+
_transfer(to, amount);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Detection Heuristics
|
|
29
|
+
1. Search for `ecrecover` or `ECDSA.recover` usage
|
|
30
|
+
2. Examine what is included in the signed hash — check for:
|
|
31
|
+
- Nonce: is there a per-signer incrementing nonce? Flag if absent (enables same-contract replay)
|
|
32
|
+
- Contract address (`address(this)`): flag if absent (enables cross-contract replay)
|
|
33
|
+
- Chain ID (`block.chainid`): flag if absent (enables cross-chain replay)
|
|
34
|
+
3. Check if processed signatures/hashes are tracked in a mapping to prevent reuse
|
|
35
|
+
4. Check if EIP-712 domain separator is used (it includes contract address and chain ID automatically)
|
|
36
|
+
5. Verify that the nonce is incremented BEFORE execution, not after (to prevent reentrancy-based replay)
|
|
37
|
+
|
|
38
|
+
## False Positives
|
|
39
|
+
- EIP-712 domain separator is used with proper nonce tracking (covers address + chainId + nonce)
|
|
40
|
+
- The signed message includes all three: nonce, contract address, and chain ID
|
|
41
|
+
- The signature authorizes a one-time action that is inherently non-replayable (e.g., EIP-2612 permit with deadline and nonce)
|
|
42
|
+
|
|
43
|
+
## Remediation
|
|
44
|
+
- Include nonce, `address(this)`, and `block.chainid` in the signed message hash
|
|
45
|
+
- Track processed nonces per signer in a mapping
|
|
46
|
+
- Use EIP-712 structured data signing with a domain separator
|
|
47
|
+
```solidity
|
|
48
|
+
mapping(address => uint256) public nonces;
|
|
49
|
+
|
|
50
|
+
function executeWithSig(address to, uint256 amount, bytes memory sig) external {
|
|
51
|
+
uint256 nonce = nonces[msg.sender]++;
|
|
52
|
+
bytes32 hash = keccak256(abi.encodePacked(
|
|
53
|
+
to, amount, nonce, address(this), block.chainid
|
|
54
|
+
));
|
|
55
|
+
bytes32 ethHash = ECDSA.toEthSignedMessageHash(hash);
|
|
56
|
+
address signer = ECDSA.recover(ethHash, sig);
|
|
57
|
+
require(signer == authorizer, "invalid sig");
|
|
58
|
+
_transfer(to, amount);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: msgvalue-loop
|
|
3
|
+
description: - `msg.value` is referenced inside a loop (`for`, `while`) or in a function called multiple times within a single external call
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# msg.value Reuse in Loops
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- `msg.value` is referenced inside a loop (`for`, `while`) or in a function called multiple times within a single external call
|
|
11
|
+
- The contract has an existing ETH balance or the logic assumes `msg.value` is "spent" per iteration
|
|
12
|
+
|
|
13
|
+
## Vulnerable Pattern
|
|
14
|
+
```solidity
|
|
15
|
+
function batchBuy(uint256[] calldata ids) external payable {
|
|
16
|
+
for (uint256 i = 0; i < ids.length; i++) {
|
|
17
|
+
// msg.value is the SAME on every iteration — it never decreases
|
|
18
|
+
// If price == 1 ETH and user sends 1 ETH, they can buy N items
|
|
19
|
+
require(msg.value >= price, "insufficient payment");
|
|
20
|
+
_mint(msg.sender, ids[i]);
|
|
21
|
+
}
|
|
22
|
+
// User paid 1 ETH but bought N items
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Payable multicall — same issue
|
|
26
|
+
function multicall(bytes[] calldata calls) external payable {
|
|
27
|
+
for (uint256 i = 0; i < calls.length; i++) {
|
|
28
|
+
// msg.value forwarded to each sub-call — reused each time
|
|
29
|
+
(bool s,) = address(this).delegatecall(calls[i]);
|
|
30
|
+
require(s);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Detection Heuristics
|
|
36
|
+
1. Search for `msg.value` usage inside `for`, `while`, or `do-while` loops
|
|
37
|
+
2. Search for `msg.value` in functions that are called via `delegatecall` in a loop (multicall patterns)
|
|
38
|
+
3. Check if `msg.value` is used in a `require` check inside a loop — passes on every iteration after a single payment
|
|
39
|
+
4. Search for internal functions that reference `msg.value` and are called multiple times from a payable external function
|
|
40
|
+
5. Check if the contract subtracts from a local tracking variable instead of relying on `msg.value` directly
|
|
41
|
+
|
|
42
|
+
## False Positives
|
|
43
|
+
- The function tracks remaining value in a local variable and decrements it per iteration (e.g., `remaining -= price`)
|
|
44
|
+
- `msg.value` is only referenced once outside any loop
|
|
45
|
+
- The loop is guaranteed to execute exactly once
|
|
46
|
+
- The function validates total cost against `msg.value` before the loop (e.g., `require(msg.value == price * ids.length)`)
|
|
47
|
+
|
|
48
|
+
## Remediation
|
|
49
|
+
- Track remaining ETH in a local variable and decrement per operation
|
|
50
|
+
- Validate total cost upfront: `require(msg.value == price * count)`
|
|
51
|
+
- In multicall patterns, ensure `msg.value` is consumed only once, or use a tracking variable
|
|
52
|
+
```solidity
|
|
53
|
+
function batchBuy(uint256[] calldata ids) external payable {
|
|
54
|
+
uint256 totalCost = price * ids.length;
|
|
55
|
+
require(msg.value >= totalCost, "insufficient payment");
|
|
56
|
+
|
|
57
|
+
for (uint256 i = 0; i < ids.length; i++) {
|
|
58
|
+
_mint(msg.sender, ids[i]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Refund excess
|
|
62
|
+
if (msg.value > totalCost) {
|
|
63
|
+
payable(msg.sender).transfer(msg.value - totalCost);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: off-by-one
|
|
3
|
+
description: - Contract uses loops with boundary conditions, comparison operators at thresholds, or array index calculations
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Off-By-One Errors
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract uses loops with boundary conditions, comparison operators at thresholds, or array index calculations
|
|
11
|
+
- The boundary/comparison is off by exactly one from the intended behavior
|
|
12
|
+
|
|
13
|
+
## Vulnerable Pattern
|
|
14
|
+
```solidity
|
|
15
|
+
// Skips the last element
|
|
16
|
+
function processAll() external {
|
|
17
|
+
for (uint256 i = 0; i < users.length - 1; i++) {
|
|
18
|
+
// Should be i < users.length
|
|
19
|
+
// Last user is never processed
|
|
20
|
+
_distribute(users[i]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Off-by-one in threshold check
|
|
25
|
+
function liquidate(uint256 ratio) external {
|
|
26
|
+
// Should be ratio < MIN_RATIO (liquidate when below)
|
|
27
|
+
// Using <= means accounts AT the minimum are also liquidated
|
|
28
|
+
require(ratio <= MIN_RATIO, "healthy");
|
|
29
|
+
_liquidate();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Out-of-bounds access
|
|
33
|
+
function getLastUser() external view returns (address) {
|
|
34
|
+
return users[users.length]; // Should be users.length - 1
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Detection Heuristics
|
|
39
|
+
1. For every loop, check the boundary condition: `< length` vs `<= length` vs `< length - 1`
|
|
40
|
+
2. `< length - 1` skips the last element — flag unless intentional
|
|
41
|
+
3. `<= length` goes out of bounds on array access — flag always
|
|
42
|
+
4. For comparison operators at thresholds (`>`, `>=`, `<`, `<=`), verify the boundary matches the specification (e.g., "greater than" vs "greater than or equal to")
|
|
43
|
+
5. Check pagination logic for fence-post errors: does the first page start at 0 or 1? Does the last batch include the final element?
|
|
44
|
+
6. Look for `length - 1` on arrays that could be empty — this underflows to `type(uint256).max` in unchecked contexts or reverts in checked contexts
|
|
45
|
+
|
|
46
|
+
## False Positives
|
|
47
|
+
- The boundary is intentionally exclusive (e.g., `< length - 1` to skip the sentinel/last element by design)
|
|
48
|
+
- The comparison operator matches the documented specification exactly
|
|
49
|
+
- The code is iterating over pairs (`i < length - 1` to compare `arr[i]` with `arr[i+1]`)
|
|
50
|
+
|
|
51
|
+
## Remediation
|
|
52
|
+
- Verify each boundary condition against the specification or documented intent
|
|
53
|
+
- Be explicit about inclusive vs exclusive bounds in comments
|
|
54
|
+
- Guard against empty array underflow: check `length > 0` before `length - 1`
|
|
55
|
+
```solidity
|
|
56
|
+
function processAll() external {
|
|
57
|
+
for (uint256 i = 0; i < users.length; i++) {
|
|
58
|
+
_distribute(users[i]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Explicit about boundary semantics
|
|
63
|
+
function liquidate(uint256 ratio) external {
|
|
64
|
+
require(ratio < MIN_RATIO, "healthy"); // Strictly below = liquidatable
|
|
65
|
+
_liquidate();
|
|
66
|
+
}
|
|
67
|
+
```
|