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,252 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oracle-manipulation
|
|
3
|
+
description: Oracle manipulation techniques, case studies, and secure pricing integration controls for DeFi.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Source: DeFiFoFum/fofum-solidity-skills (MIT) -->
|
|
7
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
8
|
+
|
|
9
|
+
# Oracle Manipulation Exploits
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Oracle manipulation attacks exploit price feeds to extract value. Attackers manipulate the price source, then use the manipulated price to profit (liquidations, swaps, borrows).
|
|
14
|
+
|
|
15
|
+
**Total Losses:** $500M+ across DeFi
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Attack Patterns
|
|
20
|
+
|
|
21
|
+
### 1. Spot Price Manipulation
|
|
22
|
+
|
|
23
|
+
Using AMM spot prices directly instead of TWAPs.
|
|
24
|
+
|
|
25
|
+
```solidity
|
|
26
|
+
// VULNERABLE: Spot price from Uniswap
|
|
27
|
+
function getPrice() external view returns (uint256) {
|
|
28
|
+
(uint112 reserve0, uint112 reserve1,) = pair.getReserves();
|
|
29
|
+
return reserve1 * 1e18 / reserve0; // Manipulatable in same tx!
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Fix:** Use TWAP (Time-Weighted Average Price) or Chainlink.
|
|
34
|
+
|
|
35
|
+
### 2. Flash Loan + Price Manipulation
|
|
36
|
+
|
|
37
|
+
1. Flash loan large amount
|
|
38
|
+
2. Swap to manipulate AMM price
|
|
39
|
+
3. Interact with victim protocol using manipulated price
|
|
40
|
+
4. Swap back, repay flash loan
|
|
41
|
+
5. Profit
|
|
42
|
+
|
|
43
|
+
### 3. Stale Price Data
|
|
44
|
+
|
|
45
|
+
```solidity
|
|
46
|
+
// VULNERABLE: No freshness check
|
|
47
|
+
function getPrice(address token) external view returns (uint256) {
|
|
48
|
+
(, int256 price,,,) = priceFeed.latestRoundData();
|
|
49
|
+
return uint256(price); // Could be hours old!
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// SECURE: Freshness validation
|
|
53
|
+
function getPrice(address token) external view returns (uint256) {
|
|
54
|
+
(
|
|
55
|
+
uint80 roundId,
|
|
56
|
+
int256 price,
|
|
57
|
+
,
|
|
58
|
+
uint256 updatedAt,
|
|
59
|
+
uint80 answeredInRound
|
|
60
|
+
) = priceFeed.latestRoundData();
|
|
61
|
+
|
|
62
|
+
require(price > 0, "Invalid price");
|
|
63
|
+
require(updatedAt > block.timestamp - MAX_DELAY, "Stale price");
|
|
64
|
+
require(answeredInRound >= roundId, "Stale round");
|
|
65
|
+
|
|
66
|
+
return uint256(price);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 4. TWAP Manipulation (Low Liquidity)
|
|
71
|
+
|
|
72
|
+
Even TWAPs can be manipulated if:
|
|
73
|
+
- Liquidity is low
|
|
74
|
+
- TWAP window is short
|
|
75
|
+
- Attacker controls multiple blocks (MEV)
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Real Exploits
|
|
80
|
+
|
|
81
|
+
### Harvest Finance (Oct 2020) — $34M
|
|
82
|
+
|
|
83
|
+
**What happened:**
|
|
84
|
+
- Attacker used flash loans to manipulate Curve pool prices
|
|
85
|
+
- Harvest used Curve spot price for USDC/USDT
|
|
86
|
+
- Deposited at manipulated (low) price, withdrew at normal price
|
|
87
|
+
|
|
88
|
+
**Root cause:** Spot price oracle on low-liquidity pool
|
|
89
|
+
|
|
90
|
+
**Lesson:** Never use spot prices. Use TWAPs with sufficient window.
|
|
91
|
+
|
|
92
|
+
### Mango Markets (Oct 2022) — $116M
|
|
93
|
+
|
|
94
|
+
**What happened:**
|
|
95
|
+
- Attacker manipulated MNGO perpetual price on Mango
|
|
96
|
+
- Used inflated collateral value to borrow all assets
|
|
97
|
+
- Price manipulation was within protocol rules (no external oracle)
|
|
98
|
+
|
|
99
|
+
**Root cause:** Self-referential oracle (own perp price as collateral value)
|
|
100
|
+
|
|
101
|
+
**Lesson:** Don't use your own market prices as oracle for that same market.
|
|
102
|
+
|
|
103
|
+
### Inverse Finance (Apr 2022) — $15.6M
|
|
104
|
+
|
|
105
|
+
**What happened:**
|
|
106
|
+
- Attacker manipulated INV/ETH SushiSwap price
|
|
107
|
+
- Used manipulated price to borrow DOLA
|
|
108
|
+
- TWAP window was only 25 minutes
|
|
109
|
+
|
|
110
|
+
**Root cause:** Short TWAP window on low-liquidity pair
|
|
111
|
+
|
|
112
|
+
**Lesson:** TWAP window must be long enough to make manipulation unprofitable.
|
|
113
|
+
|
|
114
|
+
### bZx (Multiple 2020) — $8M+
|
|
115
|
+
|
|
116
|
+
**What happened:**
|
|
117
|
+
- Series of attacks using flash loans to manipulate prices
|
|
118
|
+
- Borrowed, manipulated, liquidated in single transaction
|
|
119
|
+
|
|
120
|
+
**Root cause:** Spot price oracles vulnerable to flash loan manipulation
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Detection Checklist
|
|
125
|
+
|
|
126
|
+
- [ ] Does the contract use spot prices from AMMs?
|
|
127
|
+
- [ ] Is Chainlink used? Are staleness checks implemented?
|
|
128
|
+
- [ ] If TWAP is used, what's the window? (< 30 min = risky)
|
|
129
|
+
- [ ] Is the oracle source low liquidity?
|
|
130
|
+
- [ ] Can price be manipulated within a single transaction?
|
|
131
|
+
- [ ] Are there freshness checks on price data?
|
|
132
|
+
- [ ] Is `answeredInRound >= roundId` checked?
|
|
133
|
+
- [ ] Does the protocol validate price sanity (bounds, deviation)?
|
|
134
|
+
|
|
135
|
+
## Secure Patterns
|
|
136
|
+
|
|
137
|
+
### Chainlink with Full Validation
|
|
138
|
+
|
|
139
|
+
```solidity
|
|
140
|
+
function getPrice(address token) public view returns (uint256) {
|
|
141
|
+
AggregatorV3Interface feed = priceFeeds[token];
|
|
142
|
+
require(address(feed) != address(0), "No feed");
|
|
143
|
+
|
|
144
|
+
(
|
|
145
|
+
uint80 roundId,
|
|
146
|
+
int256 price,
|
|
147
|
+
,
|
|
148
|
+
uint256 updatedAt,
|
|
149
|
+
uint80 answeredInRound
|
|
150
|
+
) = feed.latestRoundData();
|
|
151
|
+
|
|
152
|
+
// Validation
|
|
153
|
+
require(price > 0, "Invalid price");
|
|
154
|
+
require(updatedAt > 0, "Round not complete");
|
|
155
|
+
require(answeredInRound >= roundId, "Stale round");
|
|
156
|
+
require(block.timestamp - updatedAt < HEARTBEAT, "Stale price");
|
|
157
|
+
|
|
158
|
+
return uint256(price);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Circuit Breakers
|
|
163
|
+
|
|
164
|
+
```solidity
|
|
165
|
+
uint256 public lastPrice;
|
|
166
|
+
uint256 public constant MAX_DEVIATION = 10; // 10%
|
|
167
|
+
|
|
168
|
+
function getPriceWithCircuitBreaker() external returns (uint256) {
|
|
169
|
+
uint256 newPrice = oracle.getPrice();
|
|
170
|
+
|
|
171
|
+
if (lastPrice > 0) {
|
|
172
|
+
uint256 deviation = newPrice > lastPrice
|
|
173
|
+
? (newPrice - lastPrice) * 100 / lastPrice
|
|
174
|
+
: (lastPrice - newPrice) * 100 / lastPrice;
|
|
175
|
+
require(deviation <= MAX_DEVIATION, "Price deviation too high");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
lastPrice = newPrice;
|
|
179
|
+
return newPrice;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## References
|
|
186
|
+
|
|
187
|
+
- [samczsun: Taking undercollateralized loans](https://samczsun.com/taking-undercollateralized-loans-for-fun-and-for-profit/)
|
|
188
|
+
- [Chainlink Best Practices](https://docs.chain.link/data-feeds/selecting-data-feeds)
|
|
189
|
+
- [Euler Oracle Manipulation (Omniscia)](https://omniscia.io/reports/euler-finance-oracle-manipulation/)
|
|
190
|
+
|
|
191
|
+
## Supplemental Heuristics (kadenzipfel)
|
|
192
|
+
|
|
193
|
+
## Preconditions
|
|
194
|
+
- Transaction inputs are visible in the public mempool before block inclusion
|
|
195
|
+
- The outcome of the function depends on transaction ordering
|
|
196
|
+
- An attacker can profit by observing and front-running (or sandwiching) the victim's transaction
|
|
197
|
+
|
|
198
|
+
## Vulnerable Pattern
|
|
199
|
+
```solidity
|
|
200
|
+
// DEX swap without slippage protection
|
|
201
|
+
function swap(address tokenIn, address tokenOut, uint256 amountIn) external {
|
|
202
|
+
// No minimum output amount — attacker sandwiches:
|
|
203
|
+
// 1. Front-run: buy tokenOut (price goes up)
|
|
204
|
+
// 2. Victim's swap executes at worse price
|
|
205
|
+
// 3. Back-run: sell tokenOut (profit from price impact)
|
|
206
|
+
uint256 amountOut = getAmountOut(amountIn);
|
|
207
|
+
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
|
|
208
|
+
IERC20(tokenOut).transfer(msg.sender, amountOut);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// On-chain secret submission
|
|
212
|
+
function submitAnswer(bytes32 answer) external {
|
|
213
|
+
// Answer visible in mempool — anyone can copy it
|
|
214
|
+
require(keccak256(abi.encodePacked(answer)) == targetHash);
|
|
215
|
+
_reward(msg.sender);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ERC20 approval race condition
|
|
219
|
+
// User approves 100, wants to change to 50
|
|
220
|
+
// Attacker sees approve(50) in mempool, spends 100, then spends 50 = 150 total
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Detection Heuristics
|
|
224
|
+
1. Search for DEX/swap functions: check for slippage protection (`minAmountOut` parameter) and deadline parameters — flag if absent
|
|
225
|
+
2. Search for on-chain submissions of secrets, answers, or bids — these are observable in the mempool
|
|
226
|
+
3. Search for ERC20 `approve` patterns: check if the contract sets allowance to zero before setting a new value
|
|
227
|
+
4. Look for auction/bidding logic where observing others' bids provides an advantage
|
|
228
|
+
5. Identify any function where the order of execution matters and inputs are publicly visible before inclusion
|
|
229
|
+
|
|
230
|
+
## False Positives
|
|
231
|
+
- Transaction is submitted via a private mempool (Flashbots Protect, MEV Blocker)
|
|
232
|
+
- Commit-reveal scheme is used: commitment hash submitted first, reveal in a later block
|
|
233
|
+
- Slippage protection with `minAmountOut` and `deadline` parameters are present
|
|
234
|
+
- The function's outcome is order-independent (e.g., simple deposit into a vault at a fixed rate)
|
|
235
|
+
|
|
236
|
+
## Remediation
|
|
237
|
+
- For DEX operations: require `minAmountOut` and `deadline` parameters for slippage protection
|
|
238
|
+
- For secret submissions: use commit-reveal schemes (hash commitment first, reveal later)
|
|
239
|
+
- For ERC20 approvals: use `increaseAllowance`/`decreaseAllowance` or set to zero first
|
|
240
|
+
- Consider Flashbots or private transaction relays for MEV-sensitive operations
|
|
241
|
+
```solidity
|
|
242
|
+
function swap(
|
|
243
|
+
address tokenIn, address tokenOut, uint256 amountIn,
|
|
244
|
+
uint256 minAmountOut, // Slippage protection
|
|
245
|
+
uint256 deadline // Time protection
|
|
246
|
+
) external {
|
|
247
|
+
require(block.timestamp <= deadline, "expired");
|
|
248
|
+
uint256 amountOut = getAmountOut(amountIn);
|
|
249
|
+
require(amountOut >= minAmountOut, "slippage");
|
|
250
|
+
// ... execute swap
|
|
251
|
+
}
|
|
252
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: outdated-compiler-version
|
|
3
|
+
description: - Contract is compiled with a Solidity version significantly behind the latest stable release
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Outdated Compiler Version
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract is compiled with a Solidity version significantly behind the latest stable release
|
|
11
|
+
- The compiler version used has known bugs or is missing important security features
|
|
12
|
+
|
|
13
|
+
## Vulnerable Pattern
|
|
14
|
+
```solidity
|
|
15
|
+
// Old version missing built-in overflow checks
|
|
16
|
+
pragma solidity 0.7.6;
|
|
17
|
+
|
|
18
|
+
contract Token {
|
|
19
|
+
mapping(address => uint256) public balances;
|
|
20
|
+
|
|
21
|
+
function transfer(address to, uint256 amount) external {
|
|
22
|
+
// No overflow protection — 0.7.x lacks built-in checks
|
|
23
|
+
// Requires manual SafeMath usage
|
|
24
|
+
balances[msg.sender] -= amount;
|
|
25
|
+
balances[to] += amount;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Slightly newer but still has known bugs
|
|
30
|
+
pragma solidity 0.8.0;
|
|
31
|
+
// 0.8.0 had ABI encoding bugs fixed in later patches
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Detection Heuristics
|
|
35
|
+
1. Check the `pragma solidity` version in all contract files
|
|
36
|
+
2. Compare against the latest stable Solidity release — flag if significantly outdated
|
|
37
|
+
3. Cross-reference the exact version against the Solidity known bugs list
|
|
38
|
+
4. Check if the contract misses key safety features from newer versions:
|
|
39
|
+
- <0.8.0: no built-in overflow/underflow checks
|
|
40
|
+
- <0.8.4: no custom errors (gas efficiency)
|
|
41
|
+
- <0.8.20: no PUSH0 opcode support
|
|
42
|
+
5. Flag if the version has known critical bugs (check https://solidity.readthedocs.io/en/latest/bugs.html)
|
|
43
|
+
|
|
44
|
+
## False Positives
|
|
45
|
+
- The version is intentionally chosen for compatibility with a specific deployment target or toolchain
|
|
46
|
+
- The version is recent (within 1-2 minor versions of latest) and has no known critical bugs
|
|
47
|
+
- The project has documented reasons for the specific version choice
|
|
48
|
+
|
|
49
|
+
## Remediation
|
|
50
|
+
- Upgrade to the latest stable Solidity version unless there's a specific compatibility constraint
|
|
51
|
+
- Cross-reference the current version against the known bugs list before and after upgrading
|
|
52
|
+
- When upgrading across major boundaries (e.g., 0.7 to 0.8), review all arithmetic for compatibility with built-in overflow checks
|
|
53
|
+
```solidity
|
|
54
|
+
// Use latest stable version
|
|
55
|
+
pragma solidity 0.8.24;
|
|
56
|
+
|
|
57
|
+
contract Token {
|
|
58
|
+
mapping(address => uint256) public balances;
|
|
59
|
+
|
|
60
|
+
function transfer(address to, uint256 amount) external {
|
|
61
|
+
balances[msg.sender] -= amount; // Built-in overflow check
|
|
62
|
+
balances[to] += amount;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: overflow-underflow
|
|
3
|
+
description: - Solidity <0.8.0 without SafeMath, OR
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Integer Overflow and Underflow
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Solidity <0.8.0 without SafeMath, OR
|
|
11
|
+
- Arithmetic inside `unchecked { }` blocks, OR
|
|
12
|
+
- Arithmetic inside `assembly { }` / Yul blocks, OR
|
|
13
|
+
- Type downcasting (e.g., `uint8(uint256Var)`), OR
|
|
14
|
+
- Shift operators (`<<`, `>>`) on values near type boundaries
|
|
15
|
+
|
|
16
|
+
## Vulnerable Pattern
|
|
17
|
+
```solidity
|
|
18
|
+
// Pre-0.8.0: wraps silently
|
|
19
|
+
uint256 balance = 0;
|
|
20
|
+
balance -= 1; // Underflows to 2^256 - 1
|
|
21
|
+
|
|
22
|
+
// Post-0.8.0 bypass via unchecked block
|
|
23
|
+
unchecked {
|
|
24
|
+
uint256 x = type(uint256).max;
|
|
25
|
+
x += 1; // Wraps to 0, no revert
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Type downcast truncation (all versions)
|
|
29
|
+
uint256 big = 256;
|
|
30
|
+
uint8 small = uint8(big); // Silently truncates to 0
|
|
31
|
+
|
|
32
|
+
// Assembly arithmetic (all versions)
|
|
33
|
+
assembly {
|
|
34
|
+
let x := sub(0, 1) // Underflows to max uint256
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Detection Heuristics
|
|
39
|
+
1. Check the Solidity version: if <0.8.0, flag ALL arithmetic operations not wrapped in SafeMath
|
|
40
|
+
2. If >=0.8.0, search for `unchecked` blocks and audit every arithmetic operation inside them
|
|
41
|
+
3. Search for `assembly` blocks and audit all `add`, `sub`, `mul`, `div`, `shl`, `shr` operations within
|
|
42
|
+
4. Search for type downcasts: patterns like `uint8(`, `uint16(`, `int8(`, etc. — verify the source value is bounds-checked before casting
|
|
43
|
+
5. Search for shift operations (`<<`, `>>`) and verify the operand cannot overflow the target type
|
|
44
|
+
6. Check if overflow-induced reverts on critical paths could cause DoS
|
|
45
|
+
|
|
46
|
+
## False Positives
|
|
47
|
+
- Solidity >=0.8.0 arithmetic outside of `unchecked` and `assembly` blocks (automatically checked)
|
|
48
|
+
- `unchecked` blocks used only for loop counter increments (`i++`) where overflow is impossible due to bounded loop
|
|
49
|
+
- Downcasts where the value is validated to fit the target type before casting (e.g., `require(x <= type(uint8).max)`)
|
|
50
|
+
- Assembly arithmetic on values with proven bounds
|
|
51
|
+
|
|
52
|
+
## Remediation
|
|
53
|
+
- For Solidity <0.8.0: use OpenZeppelin's `SafeMath` for all arithmetic
|
|
54
|
+
- For >=0.8.0: minimize `unchecked` blocks to only provably safe operations (e.g., bounded loop counters)
|
|
55
|
+
- Use OpenZeppelin's `SafeCast` library for all type downcasts
|
|
56
|
+
- Validate bounds before assembly arithmetic
|
|
57
|
+
```solidity
|
|
58
|
+
// Safe downcast
|
|
59
|
+
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
|
|
60
|
+
uint8 small = SafeCast.toUint8(big); // Reverts if big > 255
|
|
61
|
+
```
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reentrancy
|
|
3
|
+
description: Reentrancy attack patterns, real incidents, and defensive coding checks for Solidity protocols.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Source: DeFiFoFum/fofum-solidity-skills (MIT) -->
|
|
7
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
8
|
+
|
|
9
|
+
# Reentrancy Exploits
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Reentrancy occurs when an external call allows the callee to re-enter the calling function before state updates complete.
|
|
14
|
+
|
|
15
|
+
**Types:**
|
|
16
|
+
1. **Classic reentrancy** - Same function called again
|
|
17
|
+
2. **Cross-function** - Different function in same contract
|
|
18
|
+
3. **Cross-contract** - Function in different contract sharing state
|
|
19
|
+
4. **Read-only** - View function returns stale state during reentrancy
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## The DAO Hack (2016)
|
|
24
|
+
|
|
25
|
+
**Loss:** ~$60M (3.6M ETH)
|
|
26
|
+
**Root Cause:** Classic reentrancy
|
|
27
|
+
|
|
28
|
+
### Vulnerable Code
|
|
29
|
+
```solidity
|
|
30
|
+
function withdraw(uint _amount) public {
|
|
31
|
+
require(balances[msg.sender] >= _amount);
|
|
32
|
+
|
|
33
|
+
// External call BEFORE state update
|
|
34
|
+
(bool success,) = msg.sender.call{value: _amount}("");
|
|
35
|
+
require(success);
|
|
36
|
+
|
|
37
|
+
// State update AFTER - never reached during reentrancy
|
|
38
|
+
balances[msg.sender] -= _amount;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Attack
|
|
43
|
+
```solidity
|
|
44
|
+
contract Attacker {
|
|
45
|
+
DAO dao;
|
|
46
|
+
|
|
47
|
+
function attack() external payable {
|
|
48
|
+
dao.deposit{value: 1 ether}();
|
|
49
|
+
dao.withdraw(1 ether);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
receive() external payable {
|
|
53
|
+
if (address(dao).balance >= 1 ether) {
|
|
54
|
+
dao.withdraw(1 ether); // Re-enter before balance update
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Fix: CEI Pattern
|
|
61
|
+
```solidity
|
|
62
|
+
function withdraw(uint _amount) public {
|
|
63
|
+
require(balances[msg.sender] >= _amount);
|
|
64
|
+
|
|
65
|
+
balances[msg.sender] -= _amount; // State update FIRST
|
|
66
|
+
|
|
67
|
+
(bool success,) = msg.sender.call{value: _amount}("");
|
|
68
|
+
require(success);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Cream Finance (2021)
|
|
75
|
+
|
|
76
|
+
**Loss:** ~$130M
|
|
77
|
+
**Root Cause:** Cross-contract reentrancy via AMP token (ERC777)
|
|
78
|
+
|
|
79
|
+
### Vulnerable Pattern
|
|
80
|
+
```solidity
|
|
81
|
+
// Cream's borrow function
|
|
82
|
+
function borrow(uint amount) external {
|
|
83
|
+
// Check collateral
|
|
84
|
+
require(getAccountLiquidity(msg.sender) >= amount);
|
|
85
|
+
|
|
86
|
+
// Transfer tokens (ERC777 calls receiver hook)
|
|
87
|
+
token.transfer(msg.sender, amount); // REENTRANCY HERE
|
|
88
|
+
|
|
89
|
+
// Update borrow balance
|
|
90
|
+
borrowBalances[msg.sender] += amount;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Attack Flow
|
|
95
|
+
1. Attacker deposits collateral
|
|
96
|
+
2. Calls `borrow()` for AMP token
|
|
97
|
+
3. AMP's ERC777 hook triggers during transfer
|
|
98
|
+
4. In hook, attacker deposits more collateral + borrows again
|
|
99
|
+
5. Second borrow succeeds because first borrow's balance not updated yet
|
|
100
|
+
6. Repeat until drained
|
|
101
|
+
|
|
102
|
+
### Fix
|
|
103
|
+
```solidity
|
|
104
|
+
function borrow(uint amount) external nonReentrant {
|
|
105
|
+
require(getAccountLiquidity(msg.sender) >= amount);
|
|
106
|
+
borrowBalances[msg.sender] += amount; // Update FIRST
|
|
107
|
+
token.transfer(msg.sender, amount);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Fei Protocol / Ondo Finance (2022)
|
|
114
|
+
|
|
115
|
+
**Loss:** ~$80M
|
|
116
|
+
**Root Cause:** Read-only reentrancy
|
|
117
|
+
|
|
118
|
+
### Concept
|
|
119
|
+
```solidity
|
|
120
|
+
// Protocol A's view function
|
|
121
|
+
function getExchangeRate() public view returns (uint) {
|
|
122
|
+
return totalAssets / totalShares; // Reads state
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Protocol B uses this
|
|
126
|
+
function deposit() external {
|
|
127
|
+
uint rate = protocolA.getExchangeRate(); // Gets STALE rate
|
|
128
|
+
uint shares = amount * rate;
|
|
129
|
+
// ...
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Attack Flow
|
|
134
|
+
1. Attacker calls `withdraw()` on Protocol A
|
|
135
|
+
2. During callback (before state update), call Protocol B
|
|
136
|
+
3. Protocol B queries Protocol A's exchange rate
|
|
137
|
+
4. Rate is stale (doesn't reflect pending withdrawal)
|
|
138
|
+
5. Attacker gets more shares than deserved
|
|
139
|
+
|
|
140
|
+
### Fix
|
|
141
|
+
- Check invariants in view functions
|
|
142
|
+
- Use mutex that view functions also respect
|
|
143
|
+
- Snapshot state at start of transaction
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Rari Fuse (2022)
|
|
148
|
+
|
|
149
|
+
**Loss:** ~$80M
|
|
150
|
+
**Root Cause:** Cross-function reentrancy
|
|
151
|
+
|
|
152
|
+
### Pattern
|
|
153
|
+
```solidity
|
|
154
|
+
function exitMarket(address cToken) external {
|
|
155
|
+
// Check no borrows
|
|
156
|
+
require(borrowBalances[msg.sender][cToken] == 0);
|
|
157
|
+
|
|
158
|
+
// Update membership
|
|
159
|
+
markets[msg.sender][cToken] = false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function borrow(address cToken, uint amount) external {
|
|
163
|
+
require(markets[msg.sender][cToken] == true); // Must be in market
|
|
164
|
+
|
|
165
|
+
// Transfer - REENTRANCY POINT
|
|
166
|
+
cToken.transfer(msg.sender, amount);
|
|
167
|
+
|
|
168
|
+
borrowBalances[msg.sender][cToken] += amount;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Attack
|
|
173
|
+
1. Enter market, deposit collateral
|
|
174
|
+
2. Call `borrow()`
|
|
175
|
+
3. In callback, call `exitMarket()` - passes because borrow balance not yet updated
|
|
176
|
+
4. Now out of market but have borrowed funds
|
|
177
|
+
5. Collateral released, borrow never repaid
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Detection Checklist
|
|
182
|
+
|
|
183
|
+
- [ ] Map all external calls in each function
|
|
184
|
+
- [ ] Check state updates happen BEFORE external calls
|
|
185
|
+
- [ ] Verify `nonReentrant` modifier on vulnerable functions
|
|
186
|
+
- [ ] Check for ERC777/ERC721 callback hooks
|
|
187
|
+
- [ ] Look for cross-function shared state
|
|
188
|
+
- [ ] Check view functions for read-only reentrancy
|
|
189
|
+
- [ ] Verify flash loan callbacks don't enable reentrancy
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Patterns by Token Type
|
|
194
|
+
|
|
195
|
+
| Token Type | Reentrancy Vector |
|
|
196
|
+
|------------|-------------------|
|
|
197
|
+
| ETH | `call{value:}()` |
|
|
198
|
+
| ERC777 | `tokensToSend`, `tokensReceived` hooks |
|
|
199
|
+
| ERC721 | `onERC721Received` |
|
|
200
|
+
| ERC1155 | `onERC1155Received`, batch operations |
|
|
201
|
+
| Flash loans | Callback functions |
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Resources
|
|
206
|
+
|
|
207
|
+
- **DeFiHackLabs:** `submodules/DeFiHackLabs/test/Reentrancy/`
|
|
208
|
+
- **learn-evm-attacks:** `submodules/learn-evm-attacks/test/Reentrancy/`
|
|
209
|
+
- **SWC-107:** <https://swcregistry.io/docs/SWC-107>
|
|
210
|
+
|
|
211
|
+
## Detection Heuristics (kadenzipfel)
|
|
212
|
+
|
|
213
|
+
## Preconditions
|
|
214
|
+
- Contract makes an external call (ETH transfer, token transfer, `.call()`, `.send()`, `.transfer()`, callback hook)
|
|
215
|
+
- State is modified after the external call, not before
|
|
216
|
+
- No reentrancy guard (`nonReentrant` modifier) on the function
|
|
217
|
+
- For cross-function: two or more functions share state, and at least one makes an external call before updating that shared state
|
|
218
|
+
- For cross-contract: Contract B reads Contract A's state, and A makes an external call before updating it
|
|
219
|
+
- For read-only: Contract A has a reentrancy guard but updates state after an external call; Contract B reads A's state without sharing the same lock
|
|
220
|
+
|
|
221
|
+
## Vulnerable Pattern
|
|
222
|
+
```solidity
|
|
223
|
+
// Single-function reentrancy
|
|
224
|
+
function withdraw() external {
|
|
225
|
+
uint256 bal = balances[msg.sender];
|
|
226
|
+
// External call BEFORE state update
|
|
227
|
+
(bool success,) = msg.sender.call{value: bal}("");
|
|
228
|
+
require(success);
|
|
229
|
+
// State update AFTER external call — attacker reenters withdraw()
|
|
230
|
+
// and balances[msg.sender] is still the original value
|
|
231
|
+
balances[msg.sender] = 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Hidden external calls that trigger callbacks:
|
|
235
|
+
// ERC721._safeMint() -> onERC721Received()
|
|
236
|
+
// ERC1155.safeTransferFrom() -> onERC1155Received()
|
|
237
|
+
// ERC777 token transfers -> tokensReceived() hook
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Detection Heuristics
|
|
241
|
+
1. Identify all external calls: `.call()`, `.send()`, `.transfer()`, token transfers, `_safeMint()`, `_safeTransfer()`, ERC777/ERC1155 safe transfers
|
|
242
|
+
2. For each external call, check if any state variable is written AFTER the call in the same function
|
|
243
|
+
3. If state is written after an external call, check if a `nonReentrant` guard is present on the function — flag if absent
|
|
244
|
+
4. Check for cross-function reentrancy: does the function share state with other functions that could be called during the reentrant window?
|
|
245
|
+
5. Check for cross-contract reentrancy: does any other contract read this contract's state that is stale during the external call?
|
|
246
|
+
6. Check for hidden callbacks: `_safeMint`, `_safeTransfer`, ERC777 hooks, ERC1155 hooks — these are external calls even though they don't look like `.call()`
|
|
247
|
+
|
|
248
|
+
## False Positives
|
|
249
|
+
- State is updated BEFORE the external call (checks-effects-interactions pattern correctly followed)
|
|
250
|
+
- `nonReentrant` modifier is applied to the function
|
|
251
|
+
- The external call target is a trusted, immutable contract (e.g., WETH) with no callback mechanism
|
|
252
|
+
- The function is `view`/`pure` and cannot modify state
|
|
253
|
+
- The only state read after reentry is already finalized (e.g., immutable variables)
|
|
254
|
+
|
|
255
|
+
## Remediation
|
|
256
|
+
- Apply the checks-effects-interactions pattern: perform all state changes before any external call
|
|
257
|
+
- Add OpenZeppelin's `ReentrancyGuard` with `nonReentrant` modifier to all functions that make external calls
|
|
258
|
+
- For cross-contract reentrancy, use a shared reentrancy lock across contracts or ensure state is finalized before external calls
|
|
259
|
+
```solidity
|
|
260
|
+
function withdraw() external nonReentrant {
|
|
261
|
+
uint256 bal = balances[msg.sender];
|
|
262
|
+
balances[msg.sender] = 0; // State update BEFORE external call
|
|
263
|
+
(bool success,) = msg.sender.call{value: bal}("");
|
|
264
|
+
require(success);
|
|
265
|
+
}
|
|
266
|
+
```
|