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,298 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: access-control
|
|
3
|
+
description: Access-control exploit patterns and secure authorization approaches for privileged Solidity functions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Source: DeFiFoFum/fofum-solidity-skills (MIT) -->
|
|
7
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
8
|
+
|
|
9
|
+
# Access Control Exploits
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Access control vulnerabilities occur when unauthorized users can execute privileged functions. These are often the simplest bugs but cause catastrophic losses.
|
|
14
|
+
|
|
15
|
+
**Total Losses:** $1B+ across DeFi
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Attack Patterns
|
|
20
|
+
|
|
21
|
+
### 1. Missing Access Control
|
|
22
|
+
|
|
23
|
+
```solidity
|
|
24
|
+
// VULNERABLE: Anyone can call
|
|
25
|
+
function mint(address to, uint256 amount) external {
|
|
26
|
+
_mint(to, amount);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// SECURE: Access control
|
|
30
|
+
function mint(address to, uint256 amount) external onlyMinter {
|
|
31
|
+
_mint(to, amount);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Unprotected Initialize
|
|
36
|
+
|
|
37
|
+
```solidity
|
|
38
|
+
// VULNERABLE: Anyone can initialize
|
|
39
|
+
function initialize(address _admin) external {
|
|
40
|
+
admin = _admin;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// SECURE: Initializer modifier
|
|
44
|
+
function initialize(address _admin) external initializer {
|
|
45
|
+
admin = _admin;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. tx.origin Authentication
|
|
50
|
+
|
|
51
|
+
```solidity
|
|
52
|
+
// VULNERABLE: tx.origin can be manipulated
|
|
53
|
+
function withdraw() external {
|
|
54
|
+
require(tx.origin == owner, "Not owner");
|
|
55
|
+
payable(owner).transfer(address(this).balance);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Attack: Trick owner into calling attacker's contract
|
|
59
|
+
contract Attacker {
|
|
60
|
+
function attack(VulnerableContract target) external {
|
|
61
|
+
target.withdraw(); // tx.origin is still the owner!
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 4. Privilege Escalation
|
|
67
|
+
|
|
68
|
+
```solidity
|
|
69
|
+
// VULNERABLE: Admin can make anyone admin
|
|
70
|
+
function setAdmin(address newAdmin) external onlyAdmin {
|
|
71
|
+
admin = newAdmin; // No checks on newAdmin
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// SECURE: Multi-sig or timelock for admin changes
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 5. Signature Replay
|
|
78
|
+
|
|
79
|
+
```solidity
|
|
80
|
+
// VULNERABLE: Same signature works multiple times
|
|
81
|
+
function executeWithSig(bytes calldata data, bytes calldata sig) external {
|
|
82
|
+
address signer = recover(keccak256(data), sig);
|
|
83
|
+
require(signer == admin, "Invalid sig");
|
|
84
|
+
// Execute...
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// SECURE: Include nonce
|
|
88
|
+
mapping(address => uint256) public nonces;
|
|
89
|
+
|
|
90
|
+
function executeWithSig(bytes calldata data, uint256 nonce, bytes calldata sig) external {
|
|
91
|
+
require(nonce == nonces[msg.sender]++, "Invalid nonce");
|
|
92
|
+
bytes32 hash = keccak256(abi.encode(data, nonce, address(this), block.chainid));
|
|
93
|
+
address signer = recover(hash, sig);
|
|
94
|
+
require(signer == admin, "Invalid sig");
|
|
95
|
+
// Execute...
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Real Exploits
|
|
102
|
+
|
|
103
|
+
### Ronin Bridge (Mar 2022) — $625M
|
|
104
|
+
|
|
105
|
+
**What happened:**
|
|
106
|
+
- Attackers compromised 5 of 9 validator keys
|
|
107
|
+
- 4 keys from Sky Mavis, 1 from Axie DAO
|
|
108
|
+
- Signed fraudulent withdrawals
|
|
109
|
+
|
|
110
|
+
**Root cause:** Insufficient key distribution + social engineering
|
|
111
|
+
|
|
112
|
+
**Lesson:** Multi-sig threshold must assume some keys will be compromised.
|
|
113
|
+
|
|
114
|
+
### Parity Wallet (Nov 2017) — $150M Frozen
|
|
115
|
+
|
|
116
|
+
**What happened:**
|
|
117
|
+
- Library contract had unprotected `initWallet()`
|
|
118
|
+
- Random user called it, became owner
|
|
119
|
+
- Called `kill()`, destroyed library
|
|
120
|
+
- All wallets using library became unusable
|
|
121
|
+
|
|
122
|
+
**Root cause:** Unprotected initialize function on library contract
|
|
123
|
+
|
|
124
|
+
```solidity
|
|
125
|
+
// The fatal function
|
|
126
|
+
function initWallet(address[] _owners, uint _required, uint _daylimit) {
|
|
127
|
+
// NO ACCESS CONTROL
|
|
128
|
+
initMultiowned(_owners, _required);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Lesson:** ALL contracts need access control, especially libraries.
|
|
133
|
+
|
|
134
|
+
### Wormhole (Feb 2022) — $326M
|
|
135
|
+
|
|
136
|
+
**What happened:**
|
|
137
|
+
- Attacker exploited signature verification bug
|
|
138
|
+
- Forged guardian signatures
|
|
139
|
+
- Minted 120k ETH on Solana, bridged to Ethereum
|
|
140
|
+
|
|
141
|
+
**Root cause:** Improper signature verification in Solana program
|
|
142
|
+
|
|
143
|
+
### Poly Network (Aug 2021) — $611M
|
|
144
|
+
|
|
145
|
+
**What happened:**
|
|
146
|
+
- Attacker exploited cross-chain message verification
|
|
147
|
+
- Changed the keeper address to attacker-controlled address
|
|
148
|
+
- Drained assets across multiple chains
|
|
149
|
+
|
|
150
|
+
**Root cause:** Insufficient validation of cross-chain calls
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Detection Checklist
|
|
155
|
+
|
|
156
|
+
### Function-Level
|
|
157
|
+
- [ ] Does every state-changing function have appropriate access control?
|
|
158
|
+
- [ ] Are there functions without any modifiers that should have them?
|
|
159
|
+
- [ ] Is `onlyOwner`/`onlyAdmin` used consistently?
|
|
160
|
+
- [ ] Can critical functions be called by anyone?
|
|
161
|
+
|
|
162
|
+
### Initialization
|
|
163
|
+
- [ ] Is `initialize()` protected with `initializer` modifier?
|
|
164
|
+
- [ ] Can initialize be called multiple times?
|
|
165
|
+
- [ ] Is the implementation contract also initialized?
|
|
166
|
+
- [ ] Are all state variables properly set in initialize?
|
|
167
|
+
|
|
168
|
+
### Authentication
|
|
169
|
+
- [ ] Is `tx.origin` used anywhere? (Almost always wrong)
|
|
170
|
+
- [ ] Are signatures properly validated with nonce/chainId/address?
|
|
171
|
+
- [ ] Can signatures be replayed across chains or contracts?
|
|
172
|
+
- [ ] Is there proper domain separation in signatures?
|
|
173
|
+
|
|
174
|
+
### Privilege Management
|
|
175
|
+
- [ ] Can admin change critical parameters without timelock?
|
|
176
|
+
- [ ] Is there a way to revoke compromised admin access?
|
|
177
|
+
- [ ] Are role changes logged with events?
|
|
178
|
+
- [ ] Is there role hierarchy that could be exploited?
|
|
179
|
+
|
|
180
|
+
## Secure Patterns
|
|
181
|
+
|
|
182
|
+
### OpenZeppelin AccessControl
|
|
183
|
+
|
|
184
|
+
```solidity
|
|
185
|
+
import "@openzeppelin/contracts/access/AccessControl.sol";
|
|
186
|
+
|
|
187
|
+
contract Secure is AccessControl {
|
|
188
|
+
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
|
|
189
|
+
|
|
190
|
+
constructor() {
|
|
191
|
+
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
|
|
195
|
+
_mint(to, amount);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Two-Step Ownership Transfer
|
|
201
|
+
|
|
202
|
+
```solidity
|
|
203
|
+
address public pendingOwner;
|
|
204
|
+
|
|
205
|
+
function transferOwnership(address newOwner) external onlyOwner {
|
|
206
|
+
pendingOwner = newOwner;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function acceptOwnership() external {
|
|
210
|
+
require(msg.sender == pendingOwner, "Not pending owner");
|
|
211
|
+
owner = pendingOwner;
|
|
212
|
+
pendingOwner = address(0);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Initializer Pattern
|
|
217
|
+
|
|
218
|
+
```solidity
|
|
219
|
+
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
|
220
|
+
|
|
221
|
+
contract MyContract is Initializable {
|
|
222
|
+
address public admin;
|
|
223
|
+
|
|
224
|
+
/// @custom:oz-upgrades-unsafe-allow constructor
|
|
225
|
+
constructor() {
|
|
226
|
+
_disableInitializers(); // Protect implementation
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function initialize(address _admin) external initializer {
|
|
230
|
+
admin = _admin;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## References
|
|
238
|
+
|
|
239
|
+
- [SWC-105: Unprotected Ether Withdrawal](https://swcregistry.io/docs/SWC-105)
|
|
240
|
+
- [SWC-115: Authorization through tx.origin](https://swcregistry.io/docs/SWC-115)
|
|
241
|
+
- [OpenZeppelin Access Control](https://docs.openzeppelin.com/contracts/access-control)
|
|
242
|
+
- [Ronin Post-Mortem](https://roninblockchain.substack.com/p/community-alert-ronin-validators)
|
|
243
|
+
|
|
244
|
+
## Detection Heuristics (kadenzipfel)
|
|
245
|
+
|
|
246
|
+
## Preconditions
|
|
247
|
+
- Contract has functions that modify sensitive state (ownership, balances, fees, minting, pausing, protocol parameters)
|
|
248
|
+
- Those functions lack access control modifiers (`onlyOwner`, `onlyRole`, etc.) or `require(msg.sender == ...)` checks
|
|
249
|
+
- OR: access control exists but is incomplete (e.g., role assignments themselves are unprotected)
|
|
250
|
+
|
|
251
|
+
## Vulnerable Pattern
|
|
252
|
+
```solidity
|
|
253
|
+
address public owner;
|
|
254
|
+
uint256 public feeRate;
|
|
255
|
+
|
|
256
|
+
// Missing access control — anyone can call
|
|
257
|
+
function setFeeRate(uint256 newRate) external {
|
|
258
|
+
feeRate = newRate;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Missing access control — anyone can take ownership
|
|
262
|
+
function setOwner(address newOwner) external {
|
|
263
|
+
owner = newOwner;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Incomplete: role grant is unprotected
|
|
267
|
+
function grantRole(address user, bytes32 role) external {
|
|
268
|
+
// Missing: require(hasRole(ADMIN_ROLE, msg.sender))
|
|
269
|
+
_roles[role][user] = true;
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Detection Heuristics
|
|
274
|
+
1. Identify all `external` and `public` functions that modify state variables
|
|
275
|
+
2. For each, check if it has an access control modifier (`onlyOwner`, `onlyRole`, `whenNotPaused`, etc.) or an inline `require(msg.sender == ...)` check
|
|
276
|
+
3. If a state-changing function has no access control, flag it — especially if it modifies ownership, balances, fees, or addresses
|
|
277
|
+
4. Check that role/permission management functions themselves are protected (e.g., `grantRole` requires `ADMIN_ROLE`)
|
|
278
|
+
5. Check `initialize()` functions in upgradeable contracts — they must have an `initializer` modifier to prevent re-initialization
|
|
279
|
+
|
|
280
|
+
## False Positives
|
|
281
|
+
- The function is intentionally permissionless by design (e.g., `deposit()`, `claim()` where anyone should be able to call)
|
|
282
|
+
- Access control is enforced in an internal function that the external function always calls through
|
|
283
|
+
- The contract is an implementation behind a proxy, and access control is enforced at the proxy level
|
|
284
|
+
|
|
285
|
+
## Remediation
|
|
286
|
+
- Add explicit access control to every state-changing function that should be restricted
|
|
287
|
+
- Use OpenZeppelin's `Ownable` or `AccessControl` for standardized role management
|
|
288
|
+
- Protect `initialize()` with the `initializer` modifier
|
|
289
|
+
- Apply the principle of least privilege: each function should require the minimum role necessary
|
|
290
|
+
```solidity
|
|
291
|
+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
292
|
+
|
|
293
|
+
contract Secure is Ownable {
|
|
294
|
+
function setFeeRate(uint256 newRate) external onlyOwner {
|
|
295
|
+
feeRate = newRate;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: arbitrary-storage-location
|
|
3
|
+
description: - Contract has a dynamic array in storage
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Write to Arbitrary Storage Location
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract has a dynamic array in storage
|
|
11
|
+
- User input controls the index used for writing to that array
|
|
12
|
+
- No bounds checking on the index before the write
|
|
13
|
+
- OR: assembly-level `sstore` with a user-controlled slot value
|
|
14
|
+
|
|
15
|
+
## Vulnerable Pattern
|
|
16
|
+
```solidity
|
|
17
|
+
uint256[] public data;
|
|
18
|
+
address public owner;
|
|
19
|
+
|
|
20
|
+
function write(uint256 index, uint256 value) external {
|
|
21
|
+
// User controls index — can compute an index that maps
|
|
22
|
+
// to any storage slot via the array's storage layout:
|
|
23
|
+
// array elements start at keccak256(slot_of_length)
|
|
24
|
+
// attacker calculates index to target owner's slot
|
|
25
|
+
data[index] = value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Assembly variant
|
|
29
|
+
function writeSlot(uint256 slot, uint256 value) external {
|
|
30
|
+
assembly {
|
|
31
|
+
sstore(slot, value) // Direct arbitrary storage write
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Detection Heuristics
|
|
37
|
+
1. Search for dynamic array writes where the index comes from user input or function parameters
|
|
38
|
+
2. Check if the index is bounds-checked before the write (e.g., `require(index < data.length)`)
|
|
39
|
+
3. Search for `sstore` in assembly blocks — check if the slot parameter is user-controlled
|
|
40
|
+
4. Search for `.length` assignments on dynamic arrays (Solidity <0.6.0 allowed `array.length = X`, enabling array expansion to reach any slot)
|
|
41
|
+
5. If an unbounded array write exists, verify whether the array's keccak256-based slot layout could collide with other state variable slots
|
|
42
|
+
|
|
43
|
+
## False Positives
|
|
44
|
+
- Array index is validated against `array.length` before writing
|
|
45
|
+
- The array uses `push()` only (indices are not user-controlled)
|
|
46
|
+
- Assembly `sstore` uses a hardcoded or internally computed slot
|
|
47
|
+
- Modern Solidity (>=0.6.0) prevents direct `.length` manipulation
|
|
48
|
+
|
|
49
|
+
## Remediation
|
|
50
|
+
- Always bounds-check array indices: `require(index < data.length)`
|
|
51
|
+
- Use `push()` and `pop()` instead of direct index assignment for dynamic arrays
|
|
52
|
+
- Avoid exposing `sstore` with user-controlled slot values
|
|
53
|
+
- Use mappings instead of arrays when random-access writes are needed
|
|
54
|
+
```solidity
|
|
55
|
+
function safeWrite(uint256 index, uint256 value) external {
|
|
56
|
+
require(index < data.length, "out of bounds");
|
|
57
|
+
data[index] = value;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: assert-violation
|
|
3
|
+
description: - Contract uses `assert()` statements
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Assert Violation
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract uses `assert()` statements
|
|
11
|
+
- The `assert` condition can be reached through valid program execution (not a true invariant)
|
|
12
|
+
- OR: `assert()` is used to validate user input or external call results instead of `require()`
|
|
13
|
+
|
|
14
|
+
## Vulnerable Pattern
|
|
15
|
+
```solidity
|
|
16
|
+
function transfer(address to, uint256 amount) external {
|
|
17
|
+
// WRONG: assert used for input validation — should be require
|
|
18
|
+
// In Solidity <0.8.0, this consumes ALL remaining gas on failure
|
|
19
|
+
assert(balances[msg.sender] >= amount);
|
|
20
|
+
|
|
21
|
+
balances[msg.sender] -= amount;
|
|
22
|
+
balances[to] += amount;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function withdraw() external {
|
|
26
|
+
uint256 bal = balances[msg.sender];
|
|
27
|
+
balances[msg.sender] = 0;
|
|
28
|
+
(bool success,) = msg.sender.call{value: bal}("");
|
|
29
|
+
// WRONG: assert used to check external call result
|
|
30
|
+
assert(success);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Detection Heuristics
|
|
35
|
+
1. Search for all `assert(` calls in the codebase
|
|
36
|
+
2. For each, determine whether the condition is a true invariant (mathematically impossible to violate in a correct contract) or an input/state validation
|
|
37
|
+
3. If the condition depends on user input, external call results, or mutable external state, flag it — it should be `require()` instead
|
|
38
|
+
4. Check Solidity version: in <0.8.0, failing `assert` uses the `0xfe` INVALID opcode and consumes ALL remaining gas; in >=0.8.0, it reverts with `Panic(uint256)` error code `0x01` (refunds remaining gas but provides no custom message)
|
|
39
|
+
5. If an `assert` can be triggered by an attacker, check if the gas consumption creates a griefing vector
|
|
40
|
+
|
|
41
|
+
## False Positives
|
|
42
|
+
- The `assert` checks a genuine invariant (e.g., `assert(totalSupply == sumOfAllBalances)` after a provably correct operation)
|
|
43
|
+
- The condition is mathematically unreachable given the contract's logic
|
|
44
|
+
- Used in test code, not production contracts
|
|
45
|
+
|
|
46
|
+
## Remediation
|
|
47
|
+
- Use `require()` for input validation and external call checks — it refunds remaining gas and accepts an error message
|
|
48
|
+
- Reserve `assert()` only for invariant checks that should never fail in a correct contract
|
|
49
|
+
- In Solidity >=0.8.4, prefer custom errors with `require` for gas-efficient error handling
|
|
50
|
+
```solidity
|
|
51
|
+
// Correct: require for input validation
|
|
52
|
+
function transfer(address to, uint256 amount) external {
|
|
53
|
+
require(balances[msg.sender] >= amount, "insufficient balance");
|
|
54
|
+
balances[msg.sender] -= amount;
|
|
55
|
+
balances[to] += amount;
|
|
56
|
+
// assert is appropriate here: totalSupply should never change during transfer
|
|
57
|
+
assert(balances[msg.sender] + balances[to] == oldSum);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: asserting-contract-from-code-size
|
|
3
|
+
description: - Contract uses `extcodesize` or `address.code.length` to check whether an address is an EOA vs. a contract
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Asserting Contract from Code Size
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract uses `extcodesize` or `address.code.length` to check whether an address is an EOA vs. a contract
|
|
11
|
+
- This check gates access control, anti-bot logic, or security-sensitive operations
|
|
12
|
+
- The check assumes that code size == 0 means EOA
|
|
13
|
+
|
|
14
|
+
## Vulnerable Pattern
|
|
15
|
+
```solidity
|
|
16
|
+
modifier onlyEOA() {
|
|
17
|
+
// During constructor execution, extcodesize returns 0
|
|
18
|
+
// An attacker calling from their constructor bypasses this check
|
|
19
|
+
require(msg.sender.code.length == 0, "no contracts");
|
|
20
|
+
_;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mint() external onlyEOA {
|
|
24
|
+
_mint(msg.sender, 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Assembly variant
|
|
28
|
+
function isContract(address addr) internal view returns (bool) {
|
|
29
|
+
uint256 size;
|
|
30
|
+
assembly { size := extcodesize(addr) }
|
|
31
|
+
return size > 0; // Returns false during constructor
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Detection Heuristics
|
|
36
|
+
1. Search for `extcodesize`, `.code.length`, or helper functions named `isContract`
|
|
37
|
+
2. Check if the result is used to gate access (e.g., `require(... == 0)` to allow only EOAs)
|
|
38
|
+
3. If used for EOA-only enforcement, flag it — contracts calling from their constructor have code size 0
|
|
39
|
+
4. Also check for `tx.origin == msg.sender` as an alternative EOA check — flag it as incompatible with account abstraction (ERC-4337) and smart contract wallets
|
|
40
|
+
5. Check if the "no contracts" logic protects anything security-sensitive (minting limits, anti-bot, etc.)
|
|
41
|
+
|
|
42
|
+
## False Positives
|
|
43
|
+
- `extcodesize` is used to check if a contract EXISTS at an address (not to distinguish EOA vs contract)
|
|
44
|
+
- The check is combined with other protections that don't rely solely on code size
|
|
45
|
+
- The function has no security implications if called by a contract
|
|
46
|
+
|
|
47
|
+
## Remediation
|
|
48
|
+
- There is no fully reliable on-chain method to distinguish EOAs from contracts
|
|
49
|
+
- Redesign logic to not depend on this distinction — make the system work correctly regardless of caller type
|
|
50
|
+
- If EOA-only behavior is truly needed, consider off-chain verification (e.g., signed messages from known EOAs)
|
|
51
|
+
- Remove `isContract` checks from security-critical paths
|
|
52
|
+
```solidity
|
|
53
|
+
// Instead of gating by caller type, use per-address limits
|
|
54
|
+
mapping(address => bool) public hasMinted;
|
|
55
|
+
|
|
56
|
+
function mint() external {
|
|
57
|
+
require(!hasMinted[msg.sender], "already minted");
|
|
58
|
+
hasMinted[msg.sender] = true;
|
|
59
|
+
_mint(msg.sender, 1);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: authorization-txorigin
|
|
3
|
+
description: - Contract uses `tx.origin` for authorization or access control checks (e.g., `require(tx.origin == owner)`)
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Authorization Through tx.origin
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract uses `tx.origin` for authorization or access control checks (e.g., `require(tx.origin == owner)`)
|
|
11
|
+
- A legitimate owner/admin may interact with untrusted external contracts
|
|
12
|
+
|
|
13
|
+
## Vulnerable Pattern
|
|
14
|
+
```solidity
|
|
15
|
+
contract Wallet {
|
|
16
|
+
address public owner;
|
|
17
|
+
|
|
18
|
+
function transferTo(address to, uint256 amount) external {
|
|
19
|
+
// tx.origin is the EOA that initiated the entire tx chain
|
|
20
|
+
// If owner calls MaliciousContract, which calls Wallet.transferTo,
|
|
21
|
+
// tx.origin is still the owner — check passes
|
|
22
|
+
require(tx.origin == owner, "not owner");
|
|
23
|
+
payable(to).transfer(amount);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
contract Attacker {
|
|
28
|
+
Wallet wallet;
|
|
29
|
+
// Owner calls this (e.g., via a phishing link)
|
|
30
|
+
fallback() external {
|
|
31
|
+
// tx.origin == owner because owner initiated the tx
|
|
32
|
+
wallet.transferTo(address(this), address(wallet).balance);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Detection Heuristics
|
|
38
|
+
1. Search for all instances of `tx.origin` in the codebase
|
|
39
|
+
2. If `tx.origin` is used in a `require`, `if`, or comparison for authorization purposes, flag it
|
|
40
|
+
3. Check if `tx.origin` is compared against privileged addresses (owner, admin, etc.)
|
|
41
|
+
4. Note: `tx.origin == msg.sender` used to verify EOA status is a different pattern (see asserting-contract-from-code-size) — still flag it but for different reasons (breaks with account abstraction)
|
|
42
|
+
|
|
43
|
+
## False Positives
|
|
44
|
+
- `tx.origin` used only for logging or analytics, not for authorization
|
|
45
|
+
- `tx.origin` used in combination with `msg.sender` checks where `msg.sender` is the primary authorization mechanism
|
|
46
|
+
|
|
47
|
+
## Remediation
|
|
48
|
+
- Replace `tx.origin` with `msg.sender` for all authorization checks
|
|
49
|
+
- `msg.sender` reflects the immediate caller, not the transaction originator
|
|
50
|
+
```solidity
|
|
51
|
+
function transferTo(address to, uint256 amount) external {
|
|
52
|
+
require(msg.sender == owner, "not owner"); // Immediate caller check
|
|
53
|
+
payable(to).transfer(amount);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: default-visibility
|
|
3
|
+
description: - Functions or state variables are declared without an explicit visibility specifier
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Default Visibility
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Functions or state variables are declared without an explicit visibility specifier
|
|
11
|
+
- For functions: Solidity <0.5.0 (compiler does not enforce explicit visibility; defaults to `public`)
|
|
12
|
+
- For state variables: any Solidity version (defaults to `internal`, but developer may have intended `private`)
|
|
13
|
+
- The function or variable performs sensitive operations or holds sensitive data
|
|
14
|
+
|
|
15
|
+
## Vulnerable Pattern
|
|
16
|
+
```solidity
|
|
17
|
+
// Solidity <0.5.0: function defaults to public
|
|
18
|
+
contract Wallet {
|
|
19
|
+
address owner;
|
|
20
|
+
|
|
21
|
+
// No visibility specified — defaults to public
|
|
22
|
+
// Anyone can call this and take ownership
|
|
23
|
+
function initWallet(address _owner) {
|
|
24
|
+
owner = _owner;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Internal helper exposed as public by default
|
|
28
|
+
function _sendFunds(address to, uint256 amount) {
|
|
29
|
+
payable(to).transfer(amount);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Detection Heuristics
|
|
35
|
+
1. Check the Solidity version: if <0.5.0, search for functions without `public`, `external`, `internal`, or `private` keywords
|
|
36
|
+
2. For any version, search for state variables without explicit visibility — they default to `internal`
|
|
37
|
+
3. For each function without explicit visibility, check if it modifies state or performs sensitive operations — these are exposed as `public` by default
|
|
38
|
+
4. Look for functions prefixed with `_` (convention for internal) that lack the `internal` keyword
|
|
39
|
+
5. In Solidity >=0.5.0, the compiler requires function visibility — but state variable visibility is still optional
|
|
40
|
+
|
|
41
|
+
## False Positives
|
|
42
|
+
- Solidity >=0.5.0 contracts: the compiler enforces function visibility, so missing function visibility won't compile
|
|
43
|
+
- State variable intended to be `internal` and correctly defaults to `internal`
|
|
44
|
+
- The function is genuinely intended to be `public`
|
|
45
|
+
|
|
46
|
+
## Remediation
|
|
47
|
+
- Always specify explicit visibility for all functions and state variables
|
|
48
|
+
- Upgrade to Solidity >=0.5.0 where function visibility is compiler-enforced
|
|
49
|
+
- Review all functions to ensure visibility matches intended access level
|
|
50
|
+
```solidity
|
|
51
|
+
contract Wallet {
|
|
52
|
+
address private owner;
|
|
53
|
+
|
|
54
|
+
function initWallet(address _owner) internal {
|
|
55
|
+
owner = _owner;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _sendFunds(address to, uint256 amount) private {
|
|
59
|
+
payable(to).transfer(amount);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delegatecall-untrusted-callee
|
|
3
|
+
description: - Contract uses `delegatecall`
|
|
4
|
+
---
|
|
5
|
+
<!-- Source: kadenzipfel/smart-contract-vulnerabilities (MIT) -->
|
|
6
|
+
|
|
7
|
+
# Delegatecall to Untrusted Callee
|
|
8
|
+
|
|
9
|
+
## Preconditions
|
|
10
|
+
- Contract uses `delegatecall`
|
|
11
|
+
- The target address of the `delegatecall` is derived from user input, function parameters, or a mutable state variable settable by non-admin users
|
|
12
|
+
- OR: a proxy pattern where the implementation address can be changed by unauthorized parties
|
|
13
|
+
|
|
14
|
+
## Vulnerable Pattern
|
|
15
|
+
```solidity
|
|
16
|
+
// User-controlled delegatecall target
|
|
17
|
+
function forward(address callee, bytes calldata data) external {
|
|
18
|
+
// Attacker supplies callee = malicious contract
|
|
19
|
+
// Malicious contract overwrites storage (e.g., slot 0 = owner)
|
|
20
|
+
(bool success,) = callee.delegatecall(data);
|
|
21
|
+
require(success);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Proxy with unprotected implementation setter
|
|
25
|
+
function setImplementation(address _impl) external {
|
|
26
|
+
// Missing: require(msg.sender == admin)
|
|
27
|
+
implementation = _impl;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Detection Heuristics
|
|
32
|
+
1. Search for all `delegatecall` invocations in the codebase
|
|
33
|
+
2. For each, trace the target address: is it hardcoded, immutable, admin-only settable, or user-influenced?
|
|
34
|
+
3. If the target comes from a function parameter, calldata, or storage variable — check that only authorized roles can set it
|
|
35
|
+
4. For proxy contracts, verify that `upgradeTo` / `setImplementation` functions have proper access control
|
|
36
|
+
5. Check storage layout compatibility between the proxy and all possible implementation contracts
|
|
37
|
+
6. Flag any generic forwarding function that passes user-supplied addresses to `delegatecall`
|
|
38
|
+
|
|
39
|
+
## False Positives
|
|
40
|
+
- `delegatecall` target is hardcoded or immutable (e.g., `address immutable IMPL`)
|
|
41
|
+
- Target is set only in the constructor and cannot be changed
|
|
42
|
+
- Target is restricted to a whitelist of audited contracts
|
|
43
|
+
- Standard proxy patterns (EIP-1967, UUPS, TransparentProxy) with proper access control on upgrades
|
|
44
|
+
|
|
45
|
+
## Remediation
|
|
46
|
+
- Restrict `delegatecall` targets to trusted, immutable, or admin-only-settable addresses
|
|
47
|
+
- Use established proxy patterns (OpenZeppelin TransparentProxy, UUPS) with proper access control
|
|
48
|
+
- Never expose `delegatecall` with a user-supplied target address
|
|
49
|
+
- Verify storage layout compatibility between proxy and implementation contracts using tools like OpenZeppelin's storage layout checker
|
|
50
|
+
```solidity
|
|
51
|
+
// Safe: immutable implementation
|
|
52
|
+
address immutable implementation;
|
|
53
|
+
constructor(address _impl) {
|
|
54
|
+
implementation = _impl;
|
|
55
|
+
}
|
|
56
|
+
fallback() external payable {
|
|
57
|
+
(bool s,) = implementation.delegatecall(msg.data);
|
|
58
|
+
require(s);
|
|
59
|
+
}
|
|
60
|
+
```
|