audit-system 2.0.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/LICENSE +21 -0
- package/README.md +351 -0
- package/agents/AGENT_REGISTRY.md +150 -0
- package/agents/assumption-analyzer.json +7 -0
- package/agents/assumption-analyzer.md +37 -0
- package/agents/composition-attacker.json +7 -0
- package/agents/composition-attacker.md +46 -0
- package/agents/economic-attacker.json +7 -0
- package/agents/economic-attacker.md +43 -0
- package/agents/exploit-writer.json +7 -0
- package/agents/exploit-writer.md +48 -0
- package/agents/orchestrator.json +16 -0
- package/agents/orchestrator.md +46 -0
- package/agents/report-writer.json +7 -0
- package/agents/report-writer.md +52 -0
- package/agents/state-machine-hacker.json +7 -0
- package/agents/state-machine-hacker.md +43 -0
- package/agents/test-generator.json +7 -0
- package/agents/test-generator.md +49 -0
- package/cli.js +93 -0
- package/config.json +74 -0
- package/lib/detect-lang.js +109 -0
- package/lib/install.js +229 -0
- package/lib/utils.js +41 -0
- package/obsidian-vault/README.md +103 -0
- package/obsidian-vault/attack-patterns/state-inconsistency.md +90 -0
- package/obsidian-vault/exploits/_index.md +109 -0
- package/obsidian-vault/exploits/beanstalk-2022.md +334 -0
- package/obsidian-vault/exploits/nomad-2022.md +295 -0
- package/obsidian-vault/exploits/ronin-2022.md +251 -0
- package/obsidian-vault/exploits/wormhole-2022.md +284 -0
- package/obsidian-vault/failed-hypotheses/_template.md +77 -0
- package/obsidian-vault/hypotheses/_template.md +43 -0
- package/obsidian-vault/hypotheses/bridge-protocol-template.md +254 -0
- package/obsidian-vault/hypotheses/dex-protocol-template.md +185 -0
- package/obsidian-vault/hypotheses/governance-protocol-template.md +263 -0
- package/obsidian-vault/hypotheses/lending-protocol-template.md +218 -0
- package/obsidian-vault/hypotheses/staking-protocol-template.md +223 -0
- package/obsidian-vault/invariant-catalog/defi-invariants.md +307 -0
- package/obsidian-vault/invariant-catalog/solana-invariants.md +213 -0
- package/obsidian-vault/novel-patterns/pattern-mutation-framework.md +316 -0
- package/obsidian-vault/reports/_template.md +92 -0
- package/obsidian-vault/research/cross-protocol-analysis/.gitkeep +0 -0
- package/obsidian-vault/research/emerging-threats/.gitkeep +0 -0
- package/obsidian-vault/research/protocol-specific/.gitkeep +0 -0
- package/obsidian-vault/test-strategies/fuzzing.md +75 -0
- package/obsidian-vault/vulnerabilities/access-control.md +122 -0
- package/obsidian-vault/vulnerabilities/flash-loan-attack.md +66 -0
- package/obsidian-vault/vulnerabilities/oracle-manipulation.md +135 -0
- package/obsidian-vault/vulnerabilities/reentrancy.md +141 -0
- package/obsidian-vault/vulnerabilities/rust-unsafe-deserialization.md +128 -0
- package/obsidian-vault/vulnerabilities/solana-account-confusion.md +125 -0
- package/obsidian-vault/vulnerabilities/solana-close-account.md +141 -0
- package/obsidian-vault/vulnerabilities/solana-cpi-attacks.md +131 -0
- package/obsidian-vault/vulnerabilities/solana-signer-authorization.md +119 -0
- package/package.json +56 -0
- package/skills/audit-connect.md +385 -0
- package/skills/auditor.md +280 -0
- package/skills/exploit-generator.md +394 -0
- package/skills/novel-discovery.md +551 -0
- package/skills/test-generator.md +511 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Oracle Manipulation
|
|
2
|
+
|
|
3
|
+
tags: #vulnerability #high #critical #oracle #defi
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
Oracle manipulation attacks exploit contracts that use manipulable on-chain price sources (spot DEX prices) to make decisions about collateral values, liquidations, or swap rates.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Pattern Recognition
|
|
13
|
+
|
|
14
|
+
### Code Signals
|
|
15
|
+
- `getReserves()` from Uniswap/PancakeSwap pair
|
|
16
|
+
- `.price0CumulativeLast()` not used (spot price used instead of TWAP)
|
|
17
|
+
- Custom oracle with single source
|
|
18
|
+
- No staleness check on Chainlink
|
|
19
|
+
- `slot0` from Uniswap V3 (manipulable)
|
|
20
|
+
|
|
21
|
+
### Detection Query for Claude
|
|
22
|
+
```
|
|
23
|
+
Where does the contract read price data?
|
|
24
|
+
Is it reading spot price from a DEX?
|
|
25
|
+
Is it using TWAP or a multi-source oracle?
|
|
26
|
+
Can the price source be manipulated in a single transaction?
|
|
27
|
+
Is there a Chainlink staleness check?
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Variants
|
|
33
|
+
|
|
34
|
+
### Spot Price Manipulation (Flash Loan)
|
|
35
|
+
```
|
|
36
|
+
1. Flash loan large amount
|
|
37
|
+
2. Swap to manipulate DEX spot price
|
|
38
|
+
3. Call vulnerable function using that price
|
|
39
|
+
4. Profit from price discrepancy
|
|
40
|
+
5. Swap back + repay flash loan
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Chainlink Stale Price
|
|
44
|
+
```
|
|
45
|
+
1. Chainlink update delayed (during volatility)
|
|
46
|
+
2. Contract uses outdated price
|
|
47
|
+
3. Attacker arbitrages between real and stale price
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Low Liquidity Oracle
|
|
51
|
+
Pool has low liquidity → small capital moves price dramatically.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Attack Strategy
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
1. Identify price feed source in contract
|
|
59
|
+
2. Check if source is manipulable in one TX
|
|
60
|
+
3. Calculate flash loan needed to move price significantly
|
|
61
|
+
4. Model profit: arbitrage value - flash loan fee
|
|
62
|
+
5. Execute if profitable
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Detection Signals
|
|
68
|
+
- `IUniswapV2Pair(pair).getReserves()` for price
|
|
69
|
+
- `slot0` in Uniswap V3
|
|
70
|
+
- Missing `require(updatedAt >= block.timestamp - STALENESS_THRESHOLD)`
|
|
71
|
+
- Single oracle source with no fallback
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## PoC Template
|
|
76
|
+
|
|
77
|
+
```solidity
|
|
78
|
+
function test_oracleManipulation() public {
|
|
79
|
+
// Setup: attacker gets flash loan
|
|
80
|
+
uint256 flashAmount = 1_000_000e18;
|
|
81
|
+
|
|
82
|
+
// Step 1: Record price before manipulation
|
|
83
|
+
uint256 priceBefore = target.getPrice();
|
|
84
|
+
|
|
85
|
+
// Step 2: Manipulate DEX price
|
|
86
|
+
// (large swap in the pair used as oracle)
|
|
87
|
+
vm.startPrank(attacker);
|
|
88
|
+
dex.swap(flashAmount, 0, attacker, "");
|
|
89
|
+
|
|
90
|
+
// Step 3: Call vulnerable function with manipulated price
|
|
91
|
+
uint256 inflatedCollateral = target.getCollateralValue();
|
|
92
|
+
|
|
93
|
+
// Step 4: Exploit the price difference
|
|
94
|
+
target.borrow(inflatedCollateral);
|
|
95
|
+
|
|
96
|
+
// Step 5: Swap back
|
|
97
|
+
dex.swap(0, flashAmount, attacker, "");
|
|
98
|
+
|
|
99
|
+
vm.stopPrank();
|
|
100
|
+
|
|
101
|
+
assertGt(token.balanceOf(attacker), 0, "Exploit failed");
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Fix
|
|
108
|
+
|
|
109
|
+
```solidity
|
|
110
|
+
// Use TWAP instead of spot price
|
|
111
|
+
function getPrice() external view returns (uint256) {
|
|
112
|
+
// Uniswap V2 TWAP
|
|
113
|
+
uint256 price0Cumulative = pair.price0CumulativeLast();
|
|
114
|
+
// ... compute TWAP over 30+ minutes
|
|
115
|
+
|
|
116
|
+
// OR use Chainlink with staleness check
|
|
117
|
+
(, int256 price, , uint256 updatedAt, ) = feed.latestRoundData();
|
|
118
|
+
require(updatedAt >= block.timestamp - 1 hours, "Stale price");
|
|
119
|
+
return uint256(price);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Real World Examples
|
|
126
|
+
- Mango Markets (2022) — $114M
|
|
127
|
+
- Cream Finance Flash Loan (2021) — $130M
|
|
128
|
+
- Euler Finance Oracle (2023) — $197M
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Links
|
|
133
|
+
- [[attack-patterns/flash-loan-attack]]
|
|
134
|
+
- [[attack-patterns/price-manipulation]]
|
|
135
|
+
- [[hypotheses/oracle-twap-bypass]]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Reentrancy
|
|
2
|
+
|
|
3
|
+
tags: #vulnerability #critical #reentrancy
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
A reentrancy attack occurs when an external contract is called before state updates are finalized, allowing the attacker to recursively call back into the vulnerable function.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Pattern Recognition
|
|
13
|
+
|
|
14
|
+
### Code Signals
|
|
15
|
+
- External call (`.call()`, `.transfer()`, interface call) before state update
|
|
16
|
+
- `withdraw()` or `claimReward()` functions
|
|
17
|
+
- ETH transfers without ReentrancyGuard
|
|
18
|
+
- `nonReentrant` modifier missing
|
|
19
|
+
|
|
20
|
+
### Detection Query for Claude
|
|
21
|
+
```
|
|
22
|
+
Does this function make an external call before updating state?
|
|
23
|
+
Is there a way to recursively re-enter this function?
|
|
24
|
+
Is ReentrancyGuard applied?
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Variants
|
|
30
|
+
|
|
31
|
+
### Single-Function Reentrancy
|
|
32
|
+
Classic: `withdraw()` → receive() → `withdraw()`
|
|
33
|
+
|
|
34
|
+
### Cross-Function Reentrancy
|
|
35
|
+
`functionA()` calls external → reenter `functionB()` which reads stale state
|
|
36
|
+
|
|
37
|
+
### Cross-Contract Reentrancy
|
|
38
|
+
Contract A calls external → reenters Contract B which shares state with A
|
|
39
|
+
|
|
40
|
+
### Read-Only Reentrancy
|
|
41
|
+
Call external → reenter view function that returns stale values → use stale value in another protocol
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Attack Strategy
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
1. Deposit funds into vulnerable contract
|
|
49
|
+
2. Call withdraw()
|
|
50
|
+
3. In receive()/fallback(), call withdraw() again
|
|
51
|
+
4. State not yet updated → contract thinks attacker still has balance
|
|
52
|
+
5. Repeat until drained
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Detection Signals
|
|
58
|
+
- `external call` BEFORE `state update`
|
|
59
|
+
- No `nonReentrant` modifier
|
|
60
|
+
- ETH sent via `.call{value: amount}("")`
|
|
61
|
+
- `balances[msg.sender] = 0` AFTER the send
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## PoC Template
|
|
66
|
+
|
|
67
|
+
```solidity
|
|
68
|
+
contract ReentrancyAttacker {
|
|
69
|
+
IVulnerable target;
|
|
70
|
+
uint256 amount;
|
|
71
|
+
|
|
72
|
+
constructor(address _target) payable {
|
|
73
|
+
target = IVulnerable(_target);
|
|
74
|
+
amount = msg.value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function attack() external {
|
|
78
|
+
target.deposit{value: amount}();
|
|
79
|
+
target.withdraw(amount);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
receive() external payable {
|
|
83
|
+
if (address(target).balance >= amount) {
|
|
84
|
+
target.withdraw(amount);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Fix
|
|
93
|
+
|
|
94
|
+
```solidity
|
|
95
|
+
// Apply CEI pattern
|
|
96
|
+
function withdraw(uint256 amount) external nonReentrant {
|
|
97
|
+
require(balances[msg.sender] >= amount);
|
|
98
|
+
balances[msg.sender] -= amount; // Effect BEFORE interaction
|
|
99
|
+
(bool success, ) = msg.sender.call{value: amount}("");
|
|
100
|
+
require(success);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Solana/CPI Reentrancy Variant
|
|
107
|
+
|
|
108
|
+
In Solana, reentrancy occurs via Cross-Program Invocation (CPI):
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
Program A calls Program B via invoke() or invoke_signed()
|
|
112
|
+
Program B calls back into Program A during the CPI
|
|
113
|
+
Program A's state is mid-transaction → reads stale data
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Solana Detection
|
|
117
|
+
- CPI to program that can control execution flow
|
|
118
|
+
- State updated AFTER CPI (not before)
|
|
119
|
+
- No reentrancy guard on instruction handlers
|
|
120
|
+
- Callback accounts are not separated
|
|
121
|
+
|
|
122
|
+
### Solana Fix
|
|
123
|
+
```rust
|
|
124
|
+
// Update state BEFORE CPI call
|
|
125
|
+
ctx.accounts.user.balance = user.balance.checked_sub(amount).unwrap();
|
|
126
|
+
// Then do CPI
|
|
127
|
+
invoke(&transfer_ix, &accounts)?;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Real World Examples
|
|
131
|
+
- The DAO Hack (2016) — $60M (EVM)
|
|
132
|
+
- Cream Finance (2021) — $130M (EVM)
|
|
133
|
+
- Fei Protocol (2022) — $80M (EVM)
|
|
134
|
+
- Multiple Solana programs with CPI reentrancy
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Links
|
|
139
|
+
- [[attack-patterns/state-inconsistency]]
|
|
140
|
+
- [[test-strategies/fuzzing]]
|
|
141
|
+
- [[poc/reentrancy-poc]]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Unsafe Deserialization & Memory Safety (Rust)
|
|
2
|
+
|
|
3
|
+
tags: #vulnerability #rust #unsafe #deserialization #high
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
Smart contracts written in Rust (Solana, ink!) sometimes use `unsafe` code for performance or custom serialization. Incorrect unsafe usage can lead to memory corruption, undefined behavior, and exploitable vulnerabilities.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Pattern Recognition
|
|
13
|
+
|
|
14
|
+
### Code Signals
|
|
15
|
+
- `unsafe { }` blocks in contract code
|
|
16
|
+
- Custom `Pack`/`Unpack` trait implementations
|
|
17
|
+
- `std::mem::transmute()` usage
|
|
18
|
+
- Raw pointer dereference: `*ptr`, `ptr.read()`, `ptr.write()`
|
|
19
|
+
- Union type access
|
|
20
|
+
- `#[repr(packed)]` structs
|
|
21
|
+
- Manual Borsh deserialization without bounds checks
|
|
22
|
+
|
|
23
|
+
### Detection Query
|
|
24
|
+
```
|
|
25
|
+
Is any data deserialized with custom unsafe code?
|
|
26
|
+
Are there transmute calls that could reinterpret types?
|
|
27
|
+
Are raw pointers used without bounds checking?
|
|
28
|
+
Could crafted input cause out-of-bounds reads?
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Variants
|
|
34
|
+
|
|
35
|
+
### Bounds Check Bypass
|
|
36
|
+
```rust
|
|
37
|
+
// Unsafe: no length check before deserialization
|
|
38
|
+
unsafe {
|
|
39
|
+
let data = &*(ptr as *const UserData);
|
|
40
|
+
// If input is shorter than UserData, reads OOB memory
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Type Confusion via Transmute
|
|
45
|
+
```rust
|
|
46
|
+
// Transmute between unrelated types
|
|
47
|
+
unsafe {
|
|
48
|
+
let value: u64 = std::mem::transmute::<[u8; 8], u64>(bytes);
|
|
49
|
+
// If bytes aren't validated, can produce any u64 value
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Uninitialized Memory
|
|
54
|
+
```rust
|
|
55
|
+
// Reading uninitialized memory
|
|
56
|
+
unsafe {
|
|
57
|
+
let mut data: UserData = std::mem::zeroed();
|
|
58
|
+
// zeroed() may produce invalid enum variants
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Union Type Confusion
|
|
63
|
+
```rust
|
|
64
|
+
// Union allows reading same bytes as different types
|
|
65
|
+
unsafe {
|
|
66
|
+
u.some_field = value;
|
|
67
|
+
u.other_field; // Reading other field = type confusion
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Attack Strategy
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
1. Identify all unsafe blocks in the contract
|
|
77
|
+
2. Check if attacker-controlled input reaches unsafe code
|
|
78
|
+
3. Craft input that triggers undefined behavior
|
|
79
|
+
4. Exploit resulting memory corruption for privilege escalation
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Detection Signals
|
|
85
|
+
- Any `unsafe` in contract business logic (vs. in crypto library)
|
|
86
|
+
- Manual `Pack`/`Unpack` without bounds checks
|
|
87
|
+
- Transmuting between types of different sizes
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## PoC Concept
|
|
92
|
+
|
|
93
|
+
```rust
|
|
94
|
+
// Craft input that causes OOB read in unsafe deserialization
|
|
95
|
+
// If custom Pack doesn't check length:
|
|
96
|
+
// Send 3 bytes where 8+ expected
|
|
97
|
+
// Reads uninitialized stack memory
|
|
98
|
+
// Value determines authorization level!
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Fix
|
|
104
|
+
|
|
105
|
+
```rust
|
|
106
|
+
// Safe: use checked deserialization
|
|
107
|
+
pub fn unpack(data: &[u8]) -> Result<Self, ProgramError> {
|
|
108
|
+
if data.len() < Self::LEN {
|
|
109
|
+
return Err(ProgramError::InvalidAccountData);
|
|
110
|
+
}
|
|
111
|
+
// Safe to proceed
|
|
112
|
+
let inner = UserData::try_from_slice(data)
|
|
113
|
+
.map_err(|_| ProgramError::InvalidAccountData)?;
|
|
114
|
+
Ok(inner)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Avoid transmute entirely
|
|
118
|
+
// Use safe conversions: from_le_bytes(), from_be_bytes()
|
|
119
|
+
let value = u64::from_le_bytes(
|
|
120
|
+
bytes.try_into().map_err(|_| MyError::InvalidLength)?
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Related
|
|
127
|
+
- [[solana-account-confusion]]
|
|
128
|
+
- [[state-inconsistency]]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Account Confusion (Solana)
|
|
2
|
+
|
|
3
|
+
tags: #vulnerability #solana #account-confusion #critical
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
In Solana, all accounts are passed as inputs to instructions. If a program doesn't properly validate which account is which, an attacker can pass the same account in multiple slots or swap accounts of the same type to gain unauthorized access or manipulate state.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Pattern Recognition
|
|
13
|
+
|
|
14
|
+
### Code Signals (Anchor)
|
|
15
|
+
- `AccountInfo` without type checking
|
|
16
|
+
- `UncheckedAccount` used where validation is needed
|
|
17
|
+
- Same account type used multiple times in a single instruction
|
|
18
|
+
- Missing `#[account(owner = ...)]` constraint
|
|
19
|
+
- Missing `has_one = ...` constraint
|
|
20
|
+
- Accounts passed without checking discriminant or data
|
|
21
|
+
|
|
22
|
+
### Detection Query
|
|
23
|
+
```
|
|
24
|
+
Does each Account in the instruction have unique constraints?
|
|
25
|
+
Can any two accounts be the same Pubkey?
|
|
26
|
+
Is there a check that account A ≠ account B?
|
|
27
|
+
Are `has_one` or `seeds` constraints used?
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Variants
|
|
33
|
+
|
|
34
|
+
### Same-Account Confusion
|
|
35
|
+
Pass the same account as both user A and user B:
|
|
36
|
+
```
|
|
37
|
+
Instruction expects: [user_a: Signer, user_b: Account]
|
|
38
|
+
Attacker passes: [attacker as both user_a and user_b]
|
|
39
|
+
Impact: Attacker transfers from user A to user B = from self to self
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Type Confusion
|
|
43
|
+
Pass an account of the same type but different user:
|
|
44
|
+
```
|
|
45
|
+
Instruction expects: [vault: Account<Vault>, user_vault: Account<Vault>]
|
|
46
|
+
Attacker passes: [user A's vault as both]
|
|
47
|
+
Impact: User B's vault state manipulated via user A's vault
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Cross-Instruction Confusion
|
|
51
|
+
Same account reused across different instructions with different meanings
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Attack Strategy
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
1. Identify instruction with multiple accounts of same type
|
|
59
|
+
2. Check if there's validation they're different accounts
|
|
60
|
+
3. If not, pass same account for both parameters
|
|
61
|
+
4. This can: bypass limits, double-spend, or confuse authority
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Detection Signals
|
|
67
|
+
- Multiple `Account<T>` of same type `T` in one instruction
|
|
68
|
+
- No `#[account(address = ...)]` or `has_one` constraints
|
|
69
|
+
- No `require_keys_neq!(a, b)` checks
|
|
70
|
+
- Accounts derived from user input not validated
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## PoC Template (Anchor/TS)
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
it("account confusion exploit", async () => {
|
|
78
|
+
const victim = anchor.web3.Keypair.generate();
|
|
79
|
+
|
|
80
|
+
// Pass victim as BOTH accounts
|
|
81
|
+
const tx = await program.methods
|
|
82
|
+
.vulnerableFunction()
|
|
83
|
+
.accounts({
|
|
84
|
+
accountA: victim.publicKey,
|
|
85
|
+
accountB: victim.publicKey, // Same!
|
|
86
|
+
})
|
|
87
|
+
.signers([attacker])
|
|
88
|
+
.rpc();
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Fix
|
|
95
|
+
|
|
96
|
+
```rust
|
|
97
|
+
// In Anchor, add constraint to ensure different accounts:
|
|
98
|
+
#[derive(Accounts)]
|
|
99
|
+
pub struct Transfer<'info> {
|
|
100
|
+
#[account(mut)]
|
|
101
|
+
pub from: Signer<'info>,
|
|
102
|
+
#[account(
|
|
103
|
+
mut,
|
|
104
|
+
constraint = from.key() != to.key() @ MyError::SameAccount
|
|
105
|
+
)]
|
|
106
|
+
pub to: Account<'info, User>,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Or manually:
|
|
110
|
+
require_keys_neq!(from.key(), to.key());
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Real World Examples
|
|
116
|
+
- Solana SPL Token program: initial design allowed self-transfer
|
|
117
|
+
- Multiple Solana NFT marketplace hacks
|
|
118
|
+
- Crema Finance (2022) — account confusion in swap logic
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Links
|
|
123
|
+
- [[solana-cpi-attacks]]
|
|
124
|
+
- [[solana-signer-authorization]]
|
|
125
|
+
- [[defi-invariants]]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Close Account & Reinitialization (Solana)
|
|
2
|
+
|
|
3
|
+
tags: #vulnerability #solana #close #reinit #high
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
In Solana, accounts can be closed to reclaim rent. If not handled correctly, attackers can reinitialize closed accounts to gain unauthorized access or manipulate state.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Pattern Recognition
|
|
13
|
+
|
|
14
|
+
### Code Signals
|
|
15
|
+
- `close` instruction that sends lamports to a destination
|
|
16
|
+
- Missing discriminator check on account data
|
|
17
|
+
- No `initialized` flag
|
|
18
|
+
- Account size can change between close and reinit
|
|
19
|
+
- Old account data remains in memory after close
|
|
20
|
+
|
|
21
|
+
### Detection Query
|
|
22
|
+
```
|
|
23
|
+
Does close() zero out account data or change discriminator?
|
|
24
|
+
Can the same account address be reinitialized after close?
|
|
25
|
+
Does every instruction check if account is already initialized?
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Variants
|
|
31
|
+
|
|
32
|
+
### Reinitialization Attack
|
|
33
|
+
```
|
|
34
|
+
1. User creates account A
|
|
35
|
+
2. User closes account A (rent reclaimed)
|
|
36
|
+
3. Attacker reinitializes account A at same address
|
|
37
|
+
4. Attacker now controls what was previously user A's account
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Use-After-Close
|
|
41
|
+
```
|
|
42
|
+
1. Account data is not cleared on close
|
|
43
|
+
2. Discriminator not checked before use
|
|
44
|
+
3. Old data remains accessible
|
|
45
|
+
4. Attacker reads stale data or passes closed-but-not-cleared account
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Rent Theft
|
|
49
|
+
```
|
|
50
|
+
close instruction sends rent to wrong destination
|
|
51
|
+
Attacker drains rent from all closable accounts
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Attack Strategy
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
1. Find close instruction
|
|
60
|
+
2. Check if account data is cleared (discriminator reset)
|
|
61
|
+
3. Check if all instructions verify account discriminator
|
|
62
|
+
4. If discriminator not checked: close → reinit → exploit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Detection Signals
|
|
68
|
+
- No discriminator check in instruction handlers
|
|
69
|
+
- Close instruction doesn't zero out account data
|
|
70
|
+
- No `is_initialized` or version field
|
|
71
|
+
- Account type can be reinterpreted after reinit
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## PoC Template (Anchor/TS)
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
it("reinitialization attack", async () => {
|
|
79
|
+
// 1. Create victim's account
|
|
80
|
+
await program.methods
|
|
81
|
+
.initialize()
|
|
82
|
+
.accounts({ user: victim.publicKey })
|
|
83
|
+
.signers([victim])
|
|
84
|
+
.rpc();
|
|
85
|
+
|
|
86
|
+
// 2. Close it
|
|
87
|
+
await program.methods
|
|
88
|
+
.close()
|
|
89
|
+
.accounts({ user: victim.publicKey })
|
|
90
|
+
.signers([victim])
|
|
91
|
+
.rpc();
|
|
92
|
+
|
|
93
|
+
// 3. Reinitialize with attacker control
|
|
94
|
+
await program.methods
|
|
95
|
+
.initialize()
|
|
96
|
+
.accounts({ user: victim.publicKey }) // Same address!
|
|
97
|
+
.signers([victim]) // Attacker has keypair
|
|
98
|
+
.rpc();
|
|
99
|
+
|
|
100
|
+
// 4. Now attacker has elevated privileges
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Fix
|
|
107
|
+
|
|
108
|
+
```rust
|
|
109
|
+
// Anchor: close constraint handles this
|
|
110
|
+
#[derive(Accounts)]
|
|
111
|
+
pub struct Close<'info> {
|
|
112
|
+
#[account(
|
|
113
|
+
mut,
|
|
114
|
+
close = destination, // Anchor handles zeroing + rent return
|
|
115
|
+
constraint = user.key == owner.key
|
|
116
|
+
)]
|
|
117
|
+
pub user: Account<'info, UserData>,
|
|
118
|
+
#[account(mut)]
|
|
119
|
+
pub destination: Signer<'info>,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Manual: zero out discriminator
|
|
123
|
+
pub fn close(ctx: Context<Close>) -> Result<()> {
|
|
124
|
+
let data = ctx.accounts.user.to_account_info();
|
|
125
|
+
let mut data_bytes = data.data.borrow_mut();
|
|
126
|
+
data_bytes[0..8].copy_from_slice(&[0u8; 8]); // Clear discriminator
|
|
127
|
+
Ok(())
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Real World Examples
|
|
134
|
+
- Jet Protocol — reinitialization vulnerability
|
|
135
|
+
- Multiple Solana programs with close + reinit patterns
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Links
|
|
140
|
+
- [[solana-account-confusion]]
|
|
141
|
+
- [[solana-signer-authorization]]
|