Skip to content

tagupta/VaultGuard

Repository files navigation

VaultGuard — ERC-4626 Vault Vulnerability Analyzer

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.

The Problem

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.

Quick Start

# 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

Demo

Scanning a vulnerable contract (modeled after Resupply)

$ 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

Scanning a safe contract (with OpenZeppelin-style protections)

$ python3 -m vaultguard tests/contracts/safe_vault.sol

======================================================================
  VaultGuard Analysis Report
======================================================================

  No vulnerabilities detected.

======================================================================

Detectors

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)

Detector Details

First Depositor checks for five mitigation strategies (based on OpenZeppelin's inflation attack analysis):

  1. OpenZeppelin-style _decimalsOffset() virtual shares/assets — the recommended defence
  2. YieldBox-style inline virtual offsets (e.g. supply + 1e8, assets + 1)
  3. Morpho DAO-style initialization deposits (_mint(address(this), ...) in constructor)
  4. Minimum deposit amount enforcement in deposit() / mint()
  5. 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:

  1. Whether the numerator is a large fixed constant (e.g., 1e36)
  2. Whether the denominator comes from an external source (oracle, totalAssets(), etc.)
  3. Whether the function is related to exchange-rate computation
  4. Whether a zero-result check exists after the division

Rounding Direction detects two classes of rounding vulnerabilities:

  1. Conversion functions (convertToShares, convertToAssets) that use plain / without mulDiv with explicit Math.Rounding — enabling rounding arbitrage
  2. 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:

  1. latestRoundData() calls without checking the updatedAt timestamp against a staleness threshold
  2. Missing answeredInRound >= roundId checks for round completeness
  3. Use of the deprecated latestAnswer() which provides no freshness data at all
  4. Missing price positivity validation (answer > 0)

Slippage Protection identifies ERC-4626 vaults where:

  1. deposit() / mint() accept no minShares parameter and have no slippage check
  2. withdraw() / redeem() accept no minAssets parameter and have no slippage check
  3. No contract-level slippage infrastructure exists (e.g., _checkSlippage, ERC-5143 variant functions)

Decimal Mismatch flags oracle/pricing contracts that:

  1. Call previewRedeem, convertToAssets, or previewWithdraw on an ERC-4626 vault
  2. Use the vault's decimals() (share decimals) to normalise the result
  3. Never separately fetch the asset's decimals() — so when shareDecimals != assetDecimals, the price is wrong by a factor of 10^(shareDecimals - assetDecimals)
  4. Based on Sherlock 025-H (Sentiment Protocol, Aug 2022)

Unsafe Withdrawal Auth detects ERC-4626 withdraw/redeem overrides where:

  1. The function accepts an owner parameter (standard ERC-4626 signature)
  2. The function burns shares or transfers assets out
  3. No call to _spendAllowance(owner, msg.sender, shares) is made
  4. No delegation to super.withdraw() / super.redeem() is made
  5. No explicit msg.sender == owner check exists
  6. 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:

  1. Read raw ERC-4626 share prices via convertToAssets(), previewRedeem(), or totalAssets()/totalSupply()
  2. Use the price for security-critical decisions (collateral valuation, exchange rates, solvency checks)
  3. Apply no temporal smoothing — no TWAP, EMA, multi-block averaging, or rate-of-change circuit breaker
  4. 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

Architecture

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.

Running Tests

python3 -m unittest tests.test_detectors -v

All 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

Future Work

  • 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

About

A static analysis tool that detects ERC-4626 vault share manipulation vulnerabilities in Solidity smart contracts

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors