A static analysis tool that detects ERC-4626 vault share manipulation vulnerabilities in Solidity smart contracts. Built as a response to the Resupply Protocol exploit ($10M loss), which chained three individually minor flaws—missing first-depositor protection, integer division truncation, and a solvency check bypass—into a single-transaction, $10M drain.
ERC-4626 tokenized vaults have become the standard building block for DeFi yield, lending, and collateral systems. But they carry a systemic risk: share price inflation attacks. The pattern keeps recurring:
| Protocol | Date | Loss | Attack Vector |
|---|---|---|---|
| Venus / wUSDM (ZKsync) | Feb 2025 | $717K | Donation inflated ERC-4626 exchange rate, self-liquidation on Venus |
| Resupply Protocol | Jun 2025 | $10M | Inflation + division truncation to zero + solvency bypass |
| sDOLA / LlamaLend | Mar 2026 | $240K | Flash-loan donation into sDOLA vault, oracle consumed raw rate, forced 27 liquidations |
| ycdeal3 / RWAVault | Apr 2026 | $388K | withdraw/redeem override dropped _spendAllowance caller authorization |
Total confirmed ERC-4626 losses (Jan 2025 – May 2026): ~$10.95M
These exploits share a common anatomy: an attacker manipulates the vault's share price or exploits flawed ERC-4626 interface overrides, and the corrupted value propagates through oracles, exchange-rate calculations, and solvency checks—turning a "vault-level" issue into a "protocol-level" catastrophe.
VaultGuard detects this entire class of vulnerability.
# No external dependencies required — pure Python 3.10+
git clone <repo-url>
cd "Job Assignment Olympix"
# Scan a contract
python3 -m vaultguard path/to/contract.sol
# Scan a directory
python3 -m vaultguard contracts/
# Output JSON report
python3 -m vaultguard contracts/ --json report.json
# Run specific detectors
python3 -m vaultguard contracts/ -d first-depositor -d division-truncation$ python3 -m vaultguard tests/contracts/vulnerable_vault.sol
======================================================================
VaultGuard Analysis Report
======================================================================
Found 15 issue(s): [CRITICAL] 4, [HIGH] 5, [MEDIUM] 6
[HIGH] #1: ERC-4626 vault `VulnerableVault` is vulnerable to
first-depositor inflation attack
[CRITICAL] #2: Division in `VulnerableLendingPair._updateExchangeRate()`
can truncate to zero without validation
[HIGH] #3: Division in `VulnerableOracleConsumer._updateExchangeRate()`
can truncate to zero without validation
[MEDIUM] #4: Vault `VulnerableVault` may be vulnerable to balance
donation manipulation
[CRITICAL] #5: Solvency check `VulnerableLendingPair._isSolvent()` can
be bypassed via zero exchange rate
[MEDIUM] #6: `VulnerableVault.convertToShares()` uses plain division
without explicit rounding control
[MEDIUM] #7: `VulnerableVault.convertToAssets()` uses plain division
without explicit rounding control
[HIGH] #8: `VulnerableOracleConsumer.getPrice()` uses
`latestRoundData()` without staleness validation
[HIGH] #9: `VulnerableOracleConsumer.getLegacyPrice()` uses
deprecated `latestAnswer()` with no freshness data
[MEDIUM] #10: Vault `VulnerableVault` deposit functions lack
slippage protection
[MEDIUM] #11: Vault `VulnerableVault` withdrawal functions lack
slippage protection
[CRITICAL] #12: `UnsafeWithdrawalVault.withdraw()` overrides ERC-4626
`withdraw` without caller authorization
[CRITICAL] #13: `UnsafeWithdrawalVault.redeem()` overrides ERC-4626
`redeem` without caller authorization
[HIGH] #14: `VulnerableVaultOracle.getPrice()` consumes raw ERC-4626
share price without smoothing
[MEDIUM] #15: `VulnerableERC4626Oracle.getPrice()` assumes ERC-4626
share and asset have the same decimals
$ python3 -m vaultguard tests/contracts/safe_vault.sol
======================================================================
VaultGuard Analysis Report
======================================================================
No vulnerabilities detected.
======================================================================
| Detector | Status | Severity | Description |
|---|---|---|---|
first-depositor |
Full | HIGH/MEDIUM/LOW | ERC-4626 vaults missing inflation protections (OZ virtual offset, YieldBox offsets, Morpho seed, min deposit, dead shares) — graduated severity |
division-truncation |
Full | CRITICAL | Exchange-rate divisions where external denominators can cause truncation to zero |
donation-vector |
Full | MEDIUM/LOW | Vaults using balanceOf(address(this)) without internal accounting or virtual offsets; sweep/skim downgrades to LOW |
solvency-bypass |
Full | CRITICAL | LTV/solvency checks that collapse when exchange rate is zero |
rounding-direction |
Full | MEDIUM-HIGH | Share conversions using plain division without explicit mulDiv rounding control |
stale-oracle |
Full | HIGH | Chainlink latestRoundData() usage without staleness/freshness validation |
slippage-protection |
Full | MEDIUM | ERC-4626 deposit/withdraw functions lacking minimum output bounds (sandwich attack vector) |
decimal-mismatch |
Full | HIGH | ERC-4626 oracle/pricing contracts that assume share and asset have the same decimals |
unsafe-withdrawal-auth |
Full | CRITICAL | ERC-4626 withdraw/redeem overrides missing _spendAllowance caller authorization (ycdeal3 pattern) |
unsmoothed-price |
Full | HIGH/MEDIUM | Oracles/lending contracts consuming raw convertToAssets() without TWAP/EMA smoothing (sDOLA/Venus pattern) |
First Depositor checks for five mitigation strategies (based on OpenZeppelin's inflation attack analysis):
- OpenZeppelin-style
_decimalsOffset()virtual shares/assets — the recommended defence - YieldBox-style inline virtual offsets (e.g.
supply + 1e8,assets + 1) - Morpho DAO-style initialization deposits (
_mint(address(this), ...)in constructor) - Minimum deposit amount enforcement in
deposit()/mint() - Dead shares minted to
address(0)on first deposit
Uses graduated severity: full mitigations (1-3) suppress the finding, partial mitigations (4-5) downgrade from HIGH to MEDIUM. Also warns at LOW severity when _decimalsOffset() returns 0 (minimal protection).
Division Truncation identifies risky divisions by checking:
- Whether the numerator is a large fixed constant (e.g.,
1e36) - Whether the denominator comes from an external source (oracle,
totalAssets(), etc.) - Whether the function is related to exchange-rate computation
- Whether a zero-result check exists after the division
Rounding Direction detects two classes of rounding vulnerabilities:
- Conversion functions (
convertToShares,convertToAssets) that use plain/withoutmulDivwith explicitMath.Rounding— enabling rounding arbitrage - Preview functions (
previewMint,previewWithdraw) that round DOWN when the ERC-4626 spec requires them to round UP against the user
Stale Oracle checks Chainlink price feed consumers for:
latestRoundData()calls without checking theupdatedAttimestamp against a staleness threshold- Missing
answeredInRound >= roundIdchecks for round completeness - Use of the deprecated
latestAnswer()which provides no freshness data at all - Missing price positivity validation (
answer > 0)
Slippage Protection identifies ERC-4626 vaults where:
deposit()/mint()accept nominSharesparameter and have no slippage checkwithdraw()/redeem()accept nominAssetsparameter and have no slippage check- No contract-level slippage infrastructure exists (e.g.,
_checkSlippage, ERC-5143 variant functions)
Decimal Mismatch flags oracle/pricing contracts that:
- Call
previewRedeem,convertToAssets, orpreviewWithdrawon an ERC-4626 vault - Use the vault's
decimals()(share decimals) to normalise the result - Never separately fetch the asset's
decimals()— so whenshareDecimals != assetDecimals, the price is wrong by a factor of10^(shareDecimals - assetDecimals) - Based on Sherlock 025-H (Sentiment Protocol, Aug 2022)
Unsafe Withdrawal Auth detects ERC-4626 withdraw/redeem overrides where:
- The function accepts an
ownerparameter (standard ERC-4626 signature) - The function burns shares or transfers assets out
- No call to
_spendAllowance(owner, msg.sender, shares)is made - No delegation to
super.withdraw()/super.redeem()is made - No explicit
msg.sender == ownercheck exists - Based on the ycdeal3 / RWAVault exploit (Apr 2026, $388K) where 8 victims were drained in a single transaction
Unsmoothed Price flags oracles/lending contracts that:
- Read raw ERC-4626 share prices via
convertToAssets(),previewRedeem(), ortotalAssets()/totalSupply() - Use the price for security-critical decisions (collateral valuation, exchange rates, solvency checks)
- Apply no temporal smoothing — no TWAP, EMA, multi-block averaging, or rate-of-change circuit breaker
- Based on the sDOLA/LlamaLend exploit (Mar 2026, $240K) and Venus/wUSDM exploit (Feb 2025, $717K) where flash-loan donations spiked the raw share price consumed by lending oracles
vaultguard/
├── __init__.py
├── __main__.py # python -m vaultguard entry point
├── cli.py # CLI argument parsing, file discovery, orchestration
├── parser.py # Solidity source → structured ContractInfo
├── report.py # Terminal + JSON report generation
└── detectors/
├── __init__.py # Detector registry
├── base.py # BaseDetector ABC, Finding, Severity types
├── first_depositor.py
├── division_truncation.py
├── donation_vector.py
├── solvency_bypass.py
├── rounding_direction.py
├── stale_oracle.py
├── slippage_protection.py
├── decimal_mismatch.py
├── unsafe_withdrawal_auth.py
└── unsmoothed_price.py
tests/
├── contracts/
│ ├── vulnerable_vault.sol # Resupply-style vulnerable contract
│ └── safe_vault.sol # Contract with proper protections
└── test_detectors.py # 18 unit + integration tests
The tool uses a custom regex-based Solidity parser (no external dependencies) that extracts contract structure, inheritance, functions, and state variables. Each detector implements a BaseDetector interface with a detect(source_unit) -> list[Finding] method, making it straightforward to add new detection rules.
python3 -m unittest tests.test_detectors -vAll 69 tests should pass, covering:
- True positive detection on vulnerable contracts
- True negative (no false positives) on safe contracts
- Individual detector unit tests with inline Solidity snippets
- Parser integration tests
- Full-suite integration test
- Slither integration for deeper data-flow analysis and cross-contract taint tracking
- Foundry test generation — auto-generate fuzzing harnesses for detected vulnerabilities
- CI/CD integration — GitHub Action for continuous scanning on every PR
- Cross-contract analysis — trace share price propagation through oracle → lending → liquidation chains