Install in Claude Code
Copygit clone --depth 1 https://github.com/PurpleAILAB/Vigilo /tmp/logic-error && cp -r /tmp/logic-error/packages/opencode/skills/logic-error ~/.claude/skills/logic-errorThen start a new Claude Code session; the skill loads automatically.
Definition
SKILL.md
# Business Logic Vulnerability Analysis
**OWASP SC03:2025** - Logic Errors rank **#3** in OWASP Smart Contract Top 10 (2025). Combined with SC04 (Input Validation), these account for **34.6% of all vulnerabilities** and **$180M+ in 2024-2025 losses**, including Bunni rounding attack ($2.4M-$8.3M).
---
## Why Logic Bugs Happen (Root Causes)
### Root Cause 1: Integer-Only Arithmetic
Solidity has NO floating point. Every division truncates.
```solidity
// In normal math: 5 / 2 = 2.5
// In Solidity: 5 / 2 = 2 ← Where does 0.5 go?
uint256 fee = amount / 10000 * feeRate;
// If amount = 5000, feeRate = 100:
// 5000 / 10000 = 0 ← Division first!
// 0 * 100 = 0 ← Fee bypassed!
```
**Attacker's view**: "Every time they divide, precision is lost. I'll make sure it's lost to ME."
### Root Cause 2: Operation Order
Division before multiplication destroys precision.
```solidity
// VULNERABLE: Divide first
uint256 result = a / b * c; // @audit Precision lost!
// SECURE: Multiply first
uint256 result = a * c / b; // Preserves more precision
// Example with a=100, b=3, c=3:
// Wrong order: 100/3*3 = 33*3 = 99 (lost 1)
// Right order: 100*3/3 = 300/3 = 100 (exact)
```
**Detection**: Any `X / Y * Z` pattern is suspect. Check if `X * Z / Y` is possible.
### Root Cause 3: Rounding Direction Mismatch
Protocol rounds in attacker's favor, not protocol's favor.
```solidity
// VULNERABLE: Rounding favors user on withdraw
function withdraw(uint256 shares) external {
uint256 assets = shares * totalAssets / totalSupply; // @audit Rounds DOWN
// Attacker withdraws many small amounts
// Each time keeps the rounded-down dust
}
// SECURE: Round against the user
function withdraw(uint256 shares) external {
uint256 assets = shares * totalAssets / totalSupply; // Still rounds down
// But on DEPOSIT, also round down (user gets fewer shares)
// Protocol never loses from rounding
}
```
**Rule**: Round DOWN on withdraw (user gets less), round DOWN on deposit (user pays more).
### Root Cause 4: Missing Edge Case Handling
Zero, one, max, first, last - all create special behaviors.
```solidity
// VULNERABLE: No zero check
function distribute(uint256 amount, uint256 recipients) external {
uint256 perPerson = amount / recipients; // @audit recipients = 0 → revert
// Or recipients = 1000000 → perPerson = 0
}
```
**Attacker's view**: "What happens at the boundaries? That's where bugs hide."
---
## The Calculation Flow Map (Core Artifact)
Trace every arithmetic operation:
```
Input (amount, shares, etc.)
↓
Validation (bounds check? zero check?)
↓
Calculation (what order? division when?)
↓
Rounding (which direction? who loses?)
↓
State Update (invariant preserved?)
↓
Output (expected vs actual?)
```
Document each calculation:
| Function | Operation | Order | Rounding | Risk |
|----------|-----------|-------|----------|------|
| mint() | shares = assets * supply / total | Mul first ✓ | Down (protocol wins) | First depositor |
| fee() | fee = amount / 10000 * rate | Div first ✗ | Down → zero | Fee bypass |
| redeem() | assets = shares * total / supply | Mul first ✓ | Down (user loses) | Check |
---
## Detection Patterns
### Pattern 1: Division Before Multiplication
**Root Cause**: Operation Order
```solidity
// VULNERABLE: Precision loss
uint256 fee = amount / 10000 * feeRate;
// If amount < 10000, fee = 0 regardless of feeRate!
uint256 share = deposit / totalAssets * totalSupply;
// Small deposits get 0 shares!
```
**Attack Flow**:
1. Find function with `a / b * c` pattern
2. Input value where `a < b`
3. Result = 0, bypassing intended logic
4. Repeat to accumulate benefit
**Search Queries**:
```
Grep("/.*\\*", glob="**/*.sol")
Grep("\\*/", glob="**/*.sol")
```
**Verification Questions**:
- Can the division result in zero?
- Would reordering to `a * c / b` be safe (no overflow)?
- What's the minimum input that gives non-zero result?
### Pattern 2: First Depositor / Vault Inflation Attack
**Root Cause**: Rounding + Empty State
```solidity
// VULNERABLE: Standard ERC4626 share calculation
function deposit(uint256 assets) external returns (uint256 shares) {
if (totalSupply == 0) {
shares = assets; // @audit First depositor sets the ratio!
} else {
shares = assets * totalSupply / totalAssets;
}
}
```
**Attack Flow** (Inflation Attack):
1. Deposit 1 wei → get 1 share
2. Donate 1,000,000 tokens directly to vault (not via deposit)
3. Now: totalAssets = 1,000,001, totalSupply = 1
4. Victim deposits 999,999 → shares = 999,999 * 1 / 1,000,001 = 0
5. Victim gets 0 shares, loses entire deposit
6. Attacker redeems 1 share → gets everything
**Search Queries**:
```
Grep("totalSupply.*==.*0|totalSupply\\(\\).*==.*0", glob="**/*.sol")
Grep("balanceOf\\(address\\(this\\)\\)", glob="**/*.sol")
Grep("ERC4626|vault|shares", glob="**/*.sol")
```
**Verification Questions**:
- What happens when totalSupply = 0?
- Is there virtual shares/assets offset?
- Is there minimum deposit requirement?
- Can assets be donated without minting shares?
### Pattern 3: Unchecked Return Values
**Root Cause**: Silent Failure Assumption
```solidity
// VULNERABLE: USDT returns false instead of reverting
IERC20(token).transfer(recipient, amount); // @audit Return not checked!
// If transfer fails, execution continues with wrong state
// SECURE: Use SafeERC20
SafeERC20.safeTransfer(IERC20(token), recipient, amount);
```
**Search Queries**:
```
Grep("\\.transfer\\(|\\.transferFrom\\(", glob="**/*.sol")
Grep("safeTransfer|SafeERC20", glob="**/*.sol")
```
### Pattern 4: Integer Overflow in Unchecked Blocks
**Root Cause**: Bypassing Solidity 0.8+ Safety
```solidity
// VULNERABLE: Intentional unchecked can overflow
unchecked {
balance += amount; // @audit Can wrap to 0 if balance + amount > MAX
counter--; // @audit Can wrap to MAX if counter = 0
}
```
**Search Queries**:
```
Grep("unchecked\\s*\\{", glob="**/*.sol")
Grep(