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,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dos-gas-limit
|
|
3
|
+
description: - Contract iterates over a dynamic array or mapping whose size can grow unboundedly
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# DoS with Block Gas Limit
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract iterates over a dynamic array or mapping whose size can grow unboundedly
|
|
11
|
+
- The iteration must complete in a single transaction (no batching/pagination)
|
|
12
|
+
- OR: time-sensitive logic where block stuffing by an attacker can delay transaction inclusion
|
|
13
|
+
|
|
14
|
+
## Vulnerable Pattern
|
|
15
|
+
```solidity
|
|
16
|
+
address[] public recipients;
|
|
17
|
+
|
|
18
|
+
function addRecipient(address r) external {
|
|
19
|
+
recipients.push(r); // Array grows without bound
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Push-payment: one tx must process all recipients
|
|
23
|
+
function distributeRewards() external {
|
|
24
|
+
for (uint256 i = 0; i < recipients.length; i++) {
|
|
25
|
+
// When recipients.length grows large enough,
|
|
26
|
+
// this loop exceeds block gas limit and ALWAYS reverts
|
|
27
|
+
payable(recipients[i]).transfer(reward);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Detection Heuristics
|
|
33
|
+
1. Identify all loops (`for`, `while`) in the codebase
|
|
34
|
+
2. For each loop, check if the iteration count depends on a dynamic array or storage structure that can grow over time
|
|
35
|
+
3. If the loop is unbounded and must complete in a single transaction, flag it — it will eventually exceed the block gas limit
|
|
36
|
+
4. Check if the function supports batching or pagination (e.g., `startIndex`, `batchSize` parameters) — if not, flag it
|
|
37
|
+
5. For time-sensitive functions (auctions, deadlines, liquidations), check if an attacker could stuff blocks with high-gas transactions to delay inclusion
|
|
38
|
+
|
|
39
|
+
## False Positives
|
|
40
|
+
- Loop iterates over a fixed-size or bounded array (e.g., `uint256[10]`, array with a capped `maxLength`)
|
|
41
|
+
- Function supports paginated/batched execution across multiple transactions
|
|
42
|
+
- Loop iteration count is controlled by the caller (e.g., batch size parameter with reasonable max)
|
|
43
|
+
- The array is admin-only appendable and has a practical maximum
|
|
44
|
+
|
|
45
|
+
## Remediation
|
|
46
|
+
- Replace push-payment (contract sends to all) with pull-payment (recipients withdraw individually)
|
|
47
|
+
- If iteration is unavoidable, add batching/pagination with `startIndex` and `batchSize` parameters
|
|
48
|
+
- Cap array sizes with a maximum length check on push operations
|
|
49
|
+
- For time-sensitive logic, avoid designs where block stuffing can be profitable
|
|
50
|
+
```solidity
|
|
51
|
+
// Pull-payment pattern
|
|
52
|
+
mapping(address => uint256) public pendingWithdrawals;
|
|
53
|
+
|
|
54
|
+
function claimReward() external {
|
|
55
|
+
uint256 amount = pendingWithdrawals[msg.sender];
|
|
56
|
+
pendingWithdrawals[msg.sender] = 0;
|
|
57
|
+
payable(msg.sender).transfer(amount);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dos-revert
|
|
3
|
+
description: - Critical contract logic depends on an external call succeeding
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# DoS with (Unexpected) Revert
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Critical contract logic depends on an external call succeeding
|
|
11
|
+
- A single revert in the external call blocks the entire function
|
|
12
|
+
- OR: strict equality checks on contract balance can be violated by force-sent ETH
|
|
13
|
+
- OR: division by zero is possible due to unvalidated denominators
|
|
14
|
+
|
|
15
|
+
## Vulnerable Pattern
|
|
16
|
+
```solidity
|
|
17
|
+
// Push-payment: one reverting recipient blocks all payments
|
|
18
|
+
function payAll() external {
|
|
19
|
+
for (uint256 i = 0; i < recipients.length; i++) {
|
|
20
|
+
// If ANY recipient reverts (e.g., contract with no receive()),
|
|
21
|
+
// the entire function reverts — no one gets paid
|
|
22
|
+
require(payable(recipients[i]).send(amounts[i]), "transfer failed");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Strict balance check broken by force-sent ETH
|
|
27
|
+
function withdraw() external {
|
|
28
|
+
// Attacker sends ETH via selfdestruct, breaking this check
|
|
29
|
+
require(address(this).balance == expectedBalance, "invariant");
|
|
30
|
+
_processWithdrawal();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Division by zero
|
|
34
|
+
function distribute(uint256 totalShares) external {
|
|
35
|
+
// If totalShares == 0, this reverts and blocks the function
|
|
36
|
+
uint256 perShare = totalRewards / totalShares;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Detection Heuristics
|
|
41
|
+
1. Search for loops containing `require` or `assert` on external call results — one failure blocks all iterations
|
|
42
|
+
2. Search for push-payment patterns: contract iterating over recipients and sending ETH/tokens in one transaction
|
|
43
|
+
3. Search for strict balance equality checks (`address(this).balance ==`) — these can be broken by `selfdestruct` or coinbase rewards force-sending ETH
|
|
44
|
+
4. Search for division operations and check if the denominator can be zero
|
|
45
|
+
5. Check for `require(success)` after `.send()` or `.call()` inside loops — this turns a single recipient failure into a full DoS
|
|
46
|
+
6. Look for "highest bidder" or "king of the hill" patterns where the current leader's refund must succeed for a new leader to be set
|
|
47
|
+
|
|
48
|
+
## False Positives
|
|
49
|
+
- Pull-payment pattern is used (each recipient withdraws individually)
|
|
50
|
+
- The external call target is a trusted, known contract that will not revert
|
|
51
|
+
- Division denominator is guaranteed non-zero by prior checks or invariants
|
|
52
|
+
- Balance checks use `>=` instead of `==`
|
|
53
|
+
- The function handles individual failures gracefully (try/catch, continue on failure)
|
|
54
|
+
|
|
55
|
+
## Remediation
|
|
56
|
+
- Replace push-payment with pull-payment: let recipients withdraw individually
|
|
57
|
+
- Use `>=` instead of `==` for balance checks to tolerate force-sent ETH
|
|
58
|
+
- Validate all denominators before division: `require(totalShares > 0)`
|
|
59
|
+
- In loops, handle individual call failures without reverting the whole transaction
|
|
60
|
+
- Use try/catch for external calls where failure should not be fatal
|
|
61
|
+
```solidity
|
|
62
|
+
// Pull-payment pattern
|
|
63
|
+
mapping(address => uint256) public pendingWithdrawals;
|
|
64
|
+
|
|
65
|
+
function claimPayment() external {
|
|
66
|
+
uint256 amount = pendingWithdrawals[msg.sender];
|
|
67
|
+
require(amount > 0, "nothing to claim");
|
|
68
|
+
pendingWithdrawals[msg.sender] = 0;
|
|
69
|
+
(bool success,) = msg.sender.call{value: amount}("");
|
|
70
|
+
require(success);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flash-loan-attacks
|
|
3
|
+
description: Flash-loan attack mechanics, exploit archetypes, and mitigations for capital-amplified threats.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Source: DeFiFoFum/fofum-solidity-skills (MIT) -->
|
|
7
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
8
|
+
|
|
9
|
+
# Flash Loan Attack Exploits
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Flash loans enable borrowing massive amounts with zero collateral, repaid within the same transaction. Attackers use this capital to:
|
|
14
|
+
- Manipulate prices
|
|
15
|
+
- Exploit governance
|
|
16
|
+
- Amplify arbitrage
|
|
17
|
+
- Attack economic assumptions
|
|
18
|
+
|
|
19
|
+
**Key insight:** Any assumption about "no one would have $100M" is broken by flash loans.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Attack Patterns
|
|
24
|
+
|
|
25
|
+
### 1. Price Manipulation
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
1. Flash borrow $100M
|
|
29
|
+
2. Swap to move AMM price
|
|
30
|
+
3. Interact with victim (borrow, liquidate, swap)
|
|
31
|
+
4. Swap back
|
|
32
|
+
5. Repay flash loan
|
|
33
|
+
6. Profit
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Governance Attacks
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
1. Flash borrow governance tokens
|
|
40
|
+
2. Create proposal or vote
|
|
41
|
+
3. If snapshot-based: exploit timing
|
|
42
|
+
4. Return tokens
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```solidity
|
|
46
|
+
// VULNERABLE: Instant voting power
|
|
47
|
+
function vote(uint256 proposalId, bool support) external {
|
|
48
|
+
uint256 votes = token.balanceOf(msg.sender); // Current balance!
|
|
49
|
+
proposals[proposalId].votes += votes;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// SECURE: Historical voting power
|
|
53
|
+
function vote(uint256 proposalId, bool support) external {
|
|
54
|
+
uint256 votes = token.getPastVotes(msg.sender, proposals[proposalId].snapshot);
|
|
55
|
+
proposals[proposalId].votes += votes;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Reentrancy Amplification
|
|
60
|
+
|
|
61
|
+
Flash loans amplify reentrancy by providing initial capital:
|
|
62
|
+
1. Flash loan large amount
|
|
63
|
+
2. Deposit into vulnerable protocol
|
|
64
|
+
3. Reenter to drain more than deposited
|
|
65
|
+
4. Repay loan with profits
|
|
66
|
+
|
|
67
|
+
### 4. Collateral Manipulation
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
1. Flash loan collateral asset
|
|
71
|
+
2. Deposit as collateral in lending protocol
|
|
72
|
+
3. Borrow maximum against it
|
|
73
|
+
4. Manipulate collateral price down
|
|
74
|
+
5. Abandon position (bad debt)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Real Exploits
|
|
80
|
+
|
|
81
|
+
### Euler Finance (Mar 2023) — $197M
|
|
82
|
+
|
|
83
|
+
**What happened:**
|
|
84
|
+
- Attacker exploited donateToReserves() function
|
|
85
|
+
- Flash loaned DAI, created leveraged position
|
|
86
|
+
- Used donate function to inflate debt without updating health
|
|
87
|
+
- Triggered liquidation at favorable rate
|
|
88
|
+
|
|
89
|
+
**Root cause:** donateToReserves() increased liabilities without proper health check
|
|
90
|
+
|
|
91
|
+
**Lesson:** All state-changing functions must maintain invariants.
|
|
92
|
+
|
|
93
|
+
### Cream Finance (Oct 2021) — $130M
|
|
94
|
+
|
|
95
|
+
**What happened:**
|
|
96
|
+
- Attacker used flash loan to manipulate yUSD price
|
|
97
|
+
- Created fake collateral value through price oracle manipulation
|
|
98
|
+
- Borrowed real assets against fake collateral
|
|
99
|
+
|
|
100
|
+
**Root cause:** Oracle manipulation + flash loan capital
|
|
101
|
+
|
|
102
|
+
### Beanstalk (Apr 2022) — $182M
|
|
103
|
+
|
|
104
|
+
**What happened:**
|
|
105
|
+
- Attacker flash loaned governance tokens (BEAN + LP)
|
|
106
|
+
- Achieved quorum for malicious proposal
|
|
107
|
+
- Proposal drained treasury
|
|
108
|
+
- All in one transaction
|
|
109
|
+
|
|
110
|
+
**Root cause:** No time delay between proposal and execution
|
|
111
|
+
|
|
112
|
+
**Lesson:** Governance needs timelocks AND snapshot-based voting.
|
|
113
|
+
|
|
114
|
+
### Pancake Bunny (May 2021) — $45M
|
|
115
|
+
|
|
116
|
+
**What happened:**
|
|
117
|
+
- Flash loaned BNB
|
|
118
|
+
- Manipulated BUNNY/BNB price
|
|
119
|
+
- Minted excessive BUNNY tokens at wrong price
|
|
120
|
+
- Dumped and repaid
|
|
121
|
+
|
|
122
|
+
**Root cause:** Flash-loan-vulnerable price calculation
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Detection Checklist
|
|
127
|
+
|
|
128
|
+
- [ ] Does the protocol assume capital constraints ("no one has $100M")?
|
|
129
|
+
- [ ] Can governance actions occur in same block as token acquisition?
|
|
130
|
+
- [ ] Does the protocol use spot prices? (See oracle.md)
|
|
131
|
+
- [ ] Are there any actions profitable only with large capital?
|
|
132
|
+
- [ ] Can positions be opened and closed in same transaction profitably?
|
|
133
|
+
- [ ] Is collateral value determined at time of borrow?
|
|
134
|
+
- [ ] Are there timelocks on sensitive operations?
|
|
135
|
+
- [ ] Does the protocol track historical balances for voting?
|
|
136
|
+
|
|
137
|
+
## Flash Loan Sources
|
|
138
|
+
|
|
139
|
+
Attackers can source flash loans from:
|
|
140
|
+
- **Aave** — Largest, 0.09% fee, many assets
|
|
141
|
+
- **dYdX** — No fee (technically flash + repay)
|
|
142
|
+
- **Uniswap V2/V3** — Flash swaps, 0.3% fee
|
|
143
|
+
- **Balancer** — Flash loans, dynamic fee
|
|
144
|
+
- **Maker** — DAI flash mint (up to debt ceiling)
|
|
145
|
+
|
|
146
|
+
**Combined capital:** $10B+ available in single transaction
|
|
147
|
+
|
|
148
|
+
## Secure Patterns
|
|
149
|
+
|
|
150
|
+
### Snapshot-Based Governance
|
|
151
|
+
|
|
152
|
+
```solidity
|
|
153
|
+
// ERC20Votes pattern
|
|
154
|
+
mapping(address => Checkpoint[]) private _checkpoints;
|
|
155
|
+
|
|
156
|
+
function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) {
|
|
157
|
+
require(blockNumber < block.number, "Block not yet mined");
|
|
158
|
+
return _checkpointsLookup(_checkpoints[account], blockNumber);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Time-Delayed Actions
|
|
163
|
+
|
|
164
|
+
```solidity
|
|
165
|
+
uint256 public constant TIMELOCK = 2 days;
|
|
166
|
+
mapping(bytes32 => uint256) public pendingActions;
|
|
167
|
+
|
|
168
|
+
function queueAction(bytes32 actionHash) external onlyGovernance {
|
|
169
|
+
pendingActions[actionHash] = block.timestamp + TIMELOCK;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function executeAction(bytes32 actionHash) external {
|
|
173
|
+
require(pendingActions[actionHash] != 0, "Not queued");
|
|
174
|
+
require(block.timestamp >= pendingActions[actionHash], "Timelock");
|
|
175
|
+
delete pendingActions[actionHash];
|
|
176
|
+
// Execute
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Flash Loan Guards
|
|
181
|
+
|
|
182
|
+
```solidity
|
|
183
|
+
// Detect if called within flash loan context
|
|
184
|
+
modifier noFlashLoan() {
|
|
185
|
+
require(
|
|
186
|
+
block.number > lastDepositBlock[msg.sender],
|
|
187
|
+
"Same block"
|
|
188
|
+
);
|
|
189
|
+
_;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## References
|
|
196
|
+
|
|
197
|
+
- [Flash Loan Attack Taxonomy (Arxiv)](https://arxiv.org/abs/2003.03810)
|
|
198
|
+
- [Beanstalk Exploit Analysis](https://rekt.news/beanstalk-rekt/)
|
|
199
|
+
- [Euler Exploit (BlockSec)](https://blocksec.com/blog/how-the-200m-euler-exploit-worked)
|
|
200
|
+
|
|
201
|
+
## Supplemental Heuristics (kadenzipfel)
|
|
202
|
+
|
|
203
|
+
## Preconditions
|
|
204
|
+
- Contract uses `block.timestamp` (or the deprecated `now` alias) for security-sensitive logic
|
|
205
|
+
- The outcome of that logic can be influenced by a timestamp shift within the manipulation window (~15s on PoW chains, slot-fixed on PoS Ethereum but variable on L2s/sidechains)
|
|
206
|
+
- OR: `block.number` is used as a proxy for elapsed time
|
|
207
|
+
|
|
208
|
+
## Vulnerable Pattern
|
|
209
|
+
```solidity
|
|
210
|
+
// Timestamp as sole randomness source
|
|
211
|
+
function roll() external {
|
|
212
|
+
// Validator can manipulate block.timestamp to bias outcome
|
|
213
|
+
uint256 result = uint256(keccak256(abi.encodePacked(block.timestamp))) % 6;
|
|
214
|
+
if (result == 0) {
|
|
215
|
+
_payWinner(msg.sender);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Tight time window vulnerable to manipulation
|
|
220
|
+
function claimBonus() external {
|
|
221
|
+
// 15-second window — validator can push timestamp to include/exclude
|
|
222
|
+
require(block.timestamp >= deadline && block.timestamp <= deadline + 15);
|
|
223
|
+
_sendBonus(msg.sender);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Detection Heuristics
|
|
228
|
+
1. Search for `block.timestamp` and `now` (deprecated alias) usage
|
|
229
|
+
2. If used for randomness (e.g., fed into `keccak256` for a random value), flag immediately — this is always exploitable
|
|
230
|
+
3. If used in conditional logic, check the time window: can a ~15-second manipulation affect the outcome?
|
|
231
|
+
4. Search for `block.number` used as a time proxy (e.g., `block.number * 12` for seconds) — flag as fragile since block times change
|
|
232
|
+
5. For L2/sidechain deployments, check chain-specific timestamp guarantees — some have weaker constraints than mainnet PoS
|
|
233
|
+
|
|
234
|
+
## False Positives
|
|
235
|
+
- `block.timestamp` used only for logging or non-critical display purposes
|
|
236
|
+
- Time windows are large enough (hours/days) that a 15-second manipulation is irrelevant
|
|
237
|
+
- On PoS Ethereum mainnet, timestamps are fixed per 12-second slots — validator manipulation is constrained to slot boundaries, not arbitrary values
|
|
238
|
+
- `block.timestamp` used with a commit-reveal scheme where the timestamp alone doesn't determine the outcome
|
|
239
|
+
|
|
240
|
+
## Remediation
|
|
241
|
+
- Never use `block.timestamp` for randomness — use Chainlink VRF or another verifiable randomness oracle
|
|
242
|
+
- For time-dependent logic, ensure the acceptable window is significantly larger than the manipulation range
|
|
243
|
+
- Avoid `block.number` as a time proxy; use `block.timestamp` with appropriate tolerance
|
|
244
|
+
- On L2s/sidechains, verify chain-specific timestamp constraints before relying on `block.timestamp`
|
|
245
|
+
```solidity
|
|
246
|
+
// Safe: large time window where 15s manipulation is irrelevant
|
|
247
|
+
require(block.timestamp >= vestingEnd, "still vesting");
|
|
248
|
+
// vestingEnd is months/years away — manipulation doesn't matter
|
|
249
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: floating-pragma
|
|
3
|
+
description: - Deployable contract uses a floating or range pragma (e.g., `pragma solidity ^0.8.0`, `pragma solidity >=0.8.0`)
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Floating Pragma
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Deployable contract uses a floating or range pragma (e.g., `pragma solidity ^0.8.0`, `pragma solidity >=0.8.0`)
|
|
11
|
+
- The contract is intended for deployment (not a library or package for external consumption)
|
|
12
|
+
|
|
13
|
+
## Vulnerable Pattern
|
|
14
|
+
```solidity
|
|
15
|
+
// Floating pragma — could compile with any 0.8.x version
|
|
16
|
+
pragma solidity ^0.8.0;
|
|
17
|
+
|
|
18
|
+
contract Token {
|
|
19
|
+
// May be compiled with 0.8.0 (tested) or 0.8.25 (untested)
|
|
20
|
+
// Different compiler versions may have different bugs or behavior
|
|
21
|
+
mapping(address => uint256) public balances;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Range pragma — even wider range
|
|
25
|
+
pragma solidity >=0.7.0 <0.9.0;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Detection Heuristics
|
|
29
|
+
1. Search for `pragma solidity` declarations in all `.sol` files
|
|
30
|
+
2. If the pragma contains `^`, `>=`, `>`, or a range (e.g., `>=0.8.0 <0.9.0`), flag it as floating
|
|
31
|
+
3. Check if the file is a deployable contract or a library/package — libraries are exempt
|
|
32
|
+
4. A locked pragma looks like `pragma solidity 0.8.20;` (exact version, no caret or range)
|
|
33
|
+
5. Check if different files in the project use different Solidity versions — this creates inconsistency risk
|
|
34
|
+
|
|
35
|
+
## False Positives
|
|
36
|
+
- Libraries and packages intended for external consumption (e.g., npm packages, OpenZeppelin-style libraries) appropriately use floating pragmas for compatibility
|
|
37
|
+
- Interface files (`.sol` files containing only `interface` definitions) may use floating pragmas
|
|
38
|
+
- The floating pragma is in a test file, not a production contract
|
|
39
|
+
|
|
40
|
+
## Remediation
|
|
41
|
+
- Use locked pragmas for all deployable contracts: `pragma solidity 0.8.20;`
|
|
42
|
+
- Verify the locked version is tested and free of known compiler bugs
|
|
43
|
+
- Maintain consistent Solidity versions across the project
|
|
44
|
+
```solidity
|
|
45
|
+
// Locked pragma — deterministic compilation
|
|
46
|
+
pragma solidity 0.8.20;
|
|
47
|
+
|
|
48
|
+
contract Token {
|
|
49
|
+
mapping(address => uint256) public balances;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hash-collision
|
|
3
|
+
description: - Contract uses `abi.encodePacked()` to encode data before hashing (typically with `keccak256`)
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Hash Collision with abi.encodePacked()
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract uses `abi.encodePacked()` to encode data before hashing (typically with `keccak256`)
|
|
11
|
+
- Two or more adjacent arguments in the `encodePacked` call are variable-length types (strings, bytes, dynamic arrays)
|
|
12
|
+
- The resulting hash is used for authentication, deduplication, or signature verification
|
|
13
|
+
|
|
14
|
+
## Vulnerable Pattern
|
|
15
|
+
```solidity
|
|
16
|
+
function verify(string memory a, string memory b, bytes memory sig) external {
|
|
17
|
+
// abi.encodePacked("a", "bc") == abi.encodePacked("ab", "c")
|
|
18
|
+
// Attacker shifts bytes between arguments to forge a valid hash
|
|
19
|
+
bytes32 hash = keccak256(abi.encodePacked(a, b));
|
|
20
|
+
require(ECDSA.recover(hash, sig) == trustedSigner);
|
|
21
|
+
_execute(a, b);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Array variant:
|
|
25
|
+
// abi.encodePacked([addr1, addr2], [addr3])
|
|
26
|
+
// == abi.encodePacked([addr1], [addr2, addr3])
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Detection Heuristics
|
|
30
|
+
1. Search for `abi.encodePacked(` calls
|
|
31
|
+
2. Check how many arguments are variable-length types (string, bytes, dynamic arrays like `address[]`, `uint256[]`)
|
|
32
|
+
3. If two or more adjacent arguments are variable-length, flag it — elements can be shifted between arguments to produce an identical encoding
|
|
33
|
+
4. Check if the packed result feeds into `keccak256` for security-sensitive purposes (signature verification, access control, deduplication)
|
|
34
|
+
5. If only one argument is variable-length, or all arguments are fixed-length (address, uint256, bool), it is safe
|
|
35
|
+
|
|
36
|
+
## False Positives
|
|
37
|
+
- Only one argument is a variable-length type (no adjacent dynamic types to shift between)
|
|
38
|
+
- All arguments are fixed-length types (address, uint256, bytes32, bool, etc.)
|
|
39
|
+
- `abi.encode()` is used instead of `abi.encodePacked()` (includes length prefixes, no collision)
|
|
40
|
+
- The hash is not used for any security-sensitive purpose
|
|
41
|
+
|
|
42
|
+
## Remediation
|
|
43
|
+
- Replace `abi.encodePacked()` with `abi.encode()` — it includes length prefixes that prevent collisions
|
|
44
|
+
- If `encodePacked` must be used for gas efficiency, ensure at most one argument is a variable-length type
|
|
45
|
+
- Alternatively, separate variable-length arguments with fixed-length delimiters
|
|
46
|
+
```solidity
|
|
47
|
+
// Safe: abi.encode includes length prefixes
|
|
48
|
+
bytes32 hash = keccak256(abi.encode(a, b));
|
|
49
|
+
|
|
50
|
+
// Also safe: only one variable-length argument
|
|
51
|
+
bytes32 hash = keccak256(abi.encodePacked(fixedAddr, dynamicString));
|
|
52
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: inadherence-to-standards
|
|
3
|
+
description: - Contract claims to implement a standard (ERC20, ERC721, ERC1155, etc.) but deviates from the specification
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Inadherence to Standards
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract claims to implement a standard (ERC20, ERC721, ERC1155, etc.) but deviates from the specification
|
|
11
|
+
- OR: contract integrates external tokens assuming strict standard compliance without handling common deviations
|
|
12
|
+
|
|
13
|
+
## Vulnerable Pattern
|
|
14
|
+
```solidity
|
|
15
|
+
// Non-compliant ERC20: missing return value on transfer
|
|
16
|
+
// (matches USDT, BNB behavior — breaks callers that check return)
|
|
17
|
+
function transfer(address to, uint256 amount) external {
|
|
18
|
+
balances[msg.sender] -= amount;
|
|
19
|
+
balances[to] += amount;
|
|
20
|
+
// Missing: return true;
|
|
21
|
+
// Missing: emit Transfer(msg.sender, to, amount);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Caller assumes strict compliance — breaks on non-compliant tokens
|
|
25
|
+
function depositToken(IERC20 token, uint256 amount) external {
|
|
26
|
+
// Reverts on tokens that don't return bool (USDT)
|
|
27
|
+
require(token.transfer(address(this), amount), "transfer failed");
|
|
28
|
+
deposits[msg.sender] += amount;
|
|
29
|
+
// Bug: doesn't account for fee-on-transfer tokens
|
|
30
|
+
// Actual received amount may be less than `amount`
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Detection Heuristics
|
|
35
|
+
1. For token implementations: check that all required functions, return values, and events match the standard exactly (e.g., ERC20 requires `transfer` returns `bool` and emits `Transfer`)
|
|
36
|
+
2. For token integrations: check if `SafeERC20` is used for `transfer`/`transferFrom`/`approve` calls — raw IERC20 calls break on non-compliant tokens
|
|
37
|
+
3. Check for hardcoded assumptions: 18 decimals, no fee-on-transfer, no rebasing, no blocklists
|
|
38
|
+
4. For fee-on-transfer tokens: check if the contract uses balance-before/balance-after pattern to measure actual received amount
|
|
39
|
+
5. Check for missing `safeTransfer`/`safeTransferFrom` wrappers
|
|
40
|
+
|
|
41
|
+
## False Positives
|
|
42
|
+
- The contract explicitly documents that it only supports fully compliant ERC20 tokens and enforces this via a whitelist
|
|
43
|
+
- `SafeERC20` from OpenZeppelin is used, which handles missing return values
|
|
44
|
+
- The contract checks balance differences to account for fee-on-transfer
|
|
45
|
+
|
|
46
|
+
## Remediation
|
|
47
|
+
- For token implementations: strictly follow the standard — include all return values, events, and function signatures
|
|
48
|
+
- For token integrations: use OpenZeppelin's `SafeERC20` for all token interactions
|
|
49
|
+
- Use balance-before/balance-after pattern for fee-on-transfer support
|
|
50
|
+
- Don't hardcode decimals — read from the token contract
|
|
51
|
+
```solidity
|
|
52
|
+
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
53
|
+
using SafeERC20 for IERC20;
|
|
54
|
+
|
|
55
|
+
function depositToken(IERC20 token, uint256 amount) external {
|
|
56
|
+
uint256 balBefore = token.balanceOf(address(this));
|
|
57
|
+
token.safeTransferFrom(msg.sender, address(this), amount);
|
|
58
|
+
uint256 received = token.balanceOf(address(this)) - balBefore;
|
|
59
|
+
deposits[msg.sender] += received;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: incorrect-constructor
|
|
3
|
+
description: - Solidity version <0.4.22 where constructors are named functions matching the contract name
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Incorrect Constructor Name
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Solidity version <0.4.22 where constructors are named functions matching the contract name
|
|
11
|
+
- The function name does not exactly match the contract name (typo, case mismatch, or contract renamed without updating the constructor)
|
|
12
|
+
|
|
13
|
+
## Vulnerable Pattern
|
|
14
|
+
```solidity
|
|
15
|
+
// Solidity <0.4.22: constructor is a named function
|
|
16
|
+
contract Owned {
|
|
17
|
+
address public owner;
|
|
18
|
+
|
|
19
|
+
// Typo: "owned" != "Owned" (case mismatch)
|
|
20
|
+
// This becomes a regular public function anyone can call
|
|
21
|
+
function owned() public {
|
|
22
|
+
owner = msg.sender;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Contract renamed but constructor not updated
|
|
27
|
+
contract Treasury {
|
|
28
|
+
address public owner;
|
|
29
|
+
|
|
30
|
+
// Was "Wallet" before rename — now a regular public function
|
|
31
|
+
function Wallet() public {
|
|
32
|
+
owner = msg.sender;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Detection Heuristics
|
|
38
|
+
1. Check the Solidity version: if >=0.4.22 and the `constructor` keyword is used, this vulnerability does not apply
|
|
39
|
+
2. For <0.4.22 contracts: find the function that sets initial state (owner, parameters) and verify its name exactly matches the contract name (case-sensitive)
|
|
40
|
+
3. Search for public/external functions that set `owner` or perform one-time initialization — these may be misnamed constructors
|
|
41
|
+
4. Check if the contract was renamed at any point (git history, comments) but the constructor function was not updated
|
|
42
|
+
5. Flag any named function that appears to perform initialization logic (sets owner, initializes critical state) but doesn't match the contract name
|
|
43
|
+
|
|
44
|
+
## False Positives
|
|
45
|
+
- Solidity >=0.4.22 using the `constructor` keyword (enforced by compiler)
|
|
46
|
+
- The function is intentionally a public initializer (e.g., in proxy patterns) with proper access control
|
|
47
|
+
|
|
48
|
+
## Remediation
|
|
49
|
+
- Upgrade to Solidity >=0.4.22 and use the `constructor` keyword
|
|
50
|
+
- For legacy contracts, verify the constructor function name exactly matches the contract name
|
|
51
|
+
```solidity
|
|
52
|
+
// Modern Solidity: compiler-enforced constructor
|
|
53
|
+
contract Owned {
|
|
54
|
+
address public owner;
|
|
55
|
+
|
|
56
|
+
constructor() {
|
|
57
|
+
owner = msg.sender;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: incorrect-inheritance-order
|
|
3
|
+
description: - Contract uses multiple inheritance (`is ContractA, ContractB, ...`)
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Incorrect Inheritance Order
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract uses multiple inheritance (`is ContractA, ContractB, ...`)
|
|
11
|
+
- Two or more parent contracts define a function with the same name and signature
|
|
12
|
+
- The inheritance order (left-to-right) does not match the developer's intended resolution
|
|
13
|
+
|
|
14
|
+
## Vulnerable Pattern
|
|
15
|
+
```solidity
|
|
16
|
+
contract Ownable {
|
|
17
|
+
function owner() public view virtual returns (address) {
|
|
18
|
+
return _owner; // Returns EOA owner
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
contract Governance {
|
|
23
|
+
function owner() public view virtual returns (address) {
|
|
24
|
+
return governance; // Returns governance contract
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// C3 linearization: rightmost (Ownable) takes precedence
|
|
29
|
+
// Developer intended Governance.owner() but gets Ownable.owner()
|
|
30
|
+
contract Treasury is Governance, Ownable {
|
|
31
|
+
// owner() resolves to Ownable (rightmost) — may not be intended
|
|
32
|
+
// Should be: is Ownable, Governance (if Governance should win)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Detection Heuristics
|
|
37
|
+
1. Identify all contracts using multiple inheritance (`is A, B, C`)
|
|
38
|
+
2. For each inheritance chain, check if parent contracts define functions with the same name and signature
|
|
39
|
+
3. Verify the inheritance order follows general-to-specific (most base first, most derived last) — Solidity's C3 linearization gives precedence to the rightmost parent
|
|
40
|
+
4. Check `override` specifiers: `override(A, B)` should explicitly list which parents are being overridden
|
|
41
|
+
5. Trace the actual resolution order and compare against documented or intended behavior
|
|
42
|
+
|
|
43
|
+
## False Positives
|
|
44
|
+
- Only one parent defines the function (no ambiguity)
|
|
45
|
+
- The contract explicitly overrides the function with `override(A, B)` and provides its own implementation
|
|
46
|
+
- The inheritance order is intentionally general-to-specific and produces the correct resolution
|
|
47
|
+
|
|
48
|
+
## Remediation
|
|
49
|
+
- Order inheritance from most base to most derived (general-to-specific, left-to-right)
|
|
50
|
+
- Explicitly override conflicting functions and specify which parents: `override(ContractA, ContractB)`
|
|
51
|
+
- Test function resolution in complex hierarchies
|
|
52
|
+
```solidity
|
|
53
|
+
// Correct: general-to-specific order
|
|
54
|
+
contract Treasury is Ownable, Governance {
|
|
55
|
+
function owner() public view override(Ownable, Governance) returns (address) {
|
|
56
|
+
return Governance.owner(); // Explicit resolution
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|