Skip to content
Quantix Security Labs

Vault share price manipulation before the first deposit

How a one-unit deposit and a direct transfer could capture every later deposit into a new Quillon vault, and why a virtual share offset makes the attack lose money instead.
CriticalFirst-depositor attackResearch assessment · 9 min read
Illustration: a vault beside a tipped balance, one share on the high pan and a stack of donated assets on the low pan, with later deposits queued toward the vault.

The case at a glance

Quillon Vaults

Ethereum · YieldSecurity profileResearch case · Quantix Protocol Research

During the case
B67.8
Current profile
A82.5
To containment
45 min
To verified fix
14.1 days
  1. 03Problem

    ERC-4626 converts between assets and shares with integer division, and the standard requires deposits to round in the vault’s favour.

    Read section
  2. 06Vulnerability

    CriticalFirst-depositor attack

    Share conversion used the raw supply and asset balance with no offset, and deposit accepted a result of zero shares.

    Read section
  3. 08Remediation

    1. 01Add _decimalsOffset() = 6 and use the offset formula for both conversions
    2. 02Revert deposits and mints that produce zero shares
    3. 03Round up in previewWithdraw and previewMint

    and 2 more steps

    Read section
  4. 09Outcome

    QV-01 was fixed and verified in the remediation commit before any new vault opened.

    Risk Index B → A, +14.7 points

    Read section

Background

Quillon Vaults pools a single ERC-20 asset into ERC-4626 vaults and allocates the capital to yield strategies. Depositors receive shares; the number minted is the deposit multiplied by the share supply and divided by the vault’s total assets, rounded down.

The July 2026 review covered the vault core ahead of a release that deploys a new vault for each strategy family. New vaults start empty, and the release plan opened each one to the public in the same transaction that created it. An empty, publicly callable vault is the precondition for every share-inflation attack.

The issue was reported as finding QV-01 of the research assessment, alongside a related rounding issue in previewWithdraw (QV-05).

Architecture

Deposits and withdrawals go through QuillonVault, which owns share accounting. totalAssets() adds the idle balance to the value StrategyManager reports for deployed capital, so any token sent straight to the vault raises totalAssets() without minting a share.

The diagram highlights the vault’s empty state. The strategy valuation path is shown because it is the second way totalAssets() can move outside a deposit (QV-02).

Figure 1Quillon Vaults: components and paths involved in this case

Quillon Vaults: architecture relevant to this case7 elements and 6 connections. Highlighted components are directly involved in the case; dashed red arrows are attack paths, dotted brass arrows are control paths.deposit()transfer() to vaulttotalAssets()valuationqueued exitsparametersDepositorsdeposit / redeemFirst depositordeposits 1 unit, donatesQuillonVaultERC-4626 vault: deposits,withdrawals and shareaccountingStrategyManagerAllocates capital, harvestsyield and routes feesAmmLpStrategyProvides liquidity to anAMM pool and values thepositionWithdrawalQueueQueues withdrawals fromilliquid strategiesQuillonTimelockDelays parameter changesand upgrades
Protocol componentInvolved in this caseExternal partyAttack pathControl path

Elements:

  • Depositors (external party): deposit / redeem
  • First depositor (adversary): deposits 1 unit, donates
  • QuillonVault (protocol component, involved in this case): ERC-4626 vault: deposits, withdrawals and share accounting
  • StrategyManager (protocol component): Allocates capital, harvests yield and routes fees
  • AmmLpStrategy (protocol component): Provides liquidity to an AMM pool and values the position
  • WithdrawalQueue (protocol component): Queues withdrawals from illiquid strategies
  • QuillonTimelock (protocol component): Delays parameter changes and upgrades

Connections:

  • Depositors to QuillonVault: deposit() (data or call flow)
  • First depositor to QuillonVault: transfer() to vault (attack path)
  • QuillonVault to StrategyManager: totalAssets() (data or call flow)
  • StrategyManager to AmmLpStrategy: valuation (data or call flow)
  • QuillonVault to WithdrawalQueue: queued exits (data or call flow)
  • QuillonTimelock to StrategyManager: parameters (control path)

Components in the Quillon Vaults profile

  • QuillonVault(involved in this case)

    ERC-4626 vault: deposits, withdrawals and share accounting

  • StrategyManager

    Allocates capital, harvests yield and routes fees

  • AmmLpStrategy

    Provides liquidity to an AMM pool and values the position

  • WithdrawalQueue

    Queues withdrawals from illiquid strategies

  • QuillonTimelock

    Delays parameter changes and upgrades

Security challenge

ERC-4626 converts between assets and shares with integer division, and the standard requires deposits to round in the vault’s favour. When the supply is tiny and total assets are large, the rounding is no longer negligible: a deposit worth less than one share mints nothing.

The review had to establish whether that state was reachable on Quillon’s release path, what it would cost an attacker, and which fix removes the incentive rather than only raising the price of the attack.

Attack surface

Every input to the share conversion was examined for ways to move it outside a normal deposit or withdrawal.

Attack surface examined in Quillon Vaults
SurfaceExposureControl
totalAssets() idle balanceRises on any direct token transfer to the vaultNone at the reviewed commit
Strategy valuationLP position valued from pool spot reserves (QV-02)30-minute TWAP with a 2% deviation guard after remediation
Share supply at launchZero until the first depositVirtual offset of 10^6; zero-share revert
previewWithdraw roundingRounded down, in the user’s favour (QV-05)Rounds up on withdraw and mint paths
Transaction orderingAn attacker can front-run the first real depositNot relied on; the arithmetic fix makes ordering irrelevant

Technical investigation

Reviewers reproduced the attack in a Foundry test against the reviewed commit. The attacker deposits one base unit and receives one share, then transfers 100,001 units directly to the vault. totalAssets() now includes the donation while the supply is still one share.

The next depositor sends 100,000 units and receives 100,000e6 × 1 / (100,001e6 + 1), which rounds down to zero shares. The deposit succeeds, and the attacker’s single share now redeems the donation, the original unit and the victim’s 100,000 units.

The first table is computed from the conversion formula. It compares the reviewed implementation with two virtual-offset settings for the same donation and deposit. With an offset of 10^6 the victim receives shares worth their deposit and the attacker forfeits half of the donation, so the attack no longer pays.

An invariant fuzzing campaign with the property “a deposit that mints zero shares reverts” found the same counterexample independently. The property is now INV-03 in the protocol’s invariant suite.

Figure 2Exploit sequence against an empty vault

Exploit sequence against an empty vaultAttackerQuillonVaultDeposit assetDepositor1. deposit(1)mints 1 share2. transfer(vault, 100,001)no shares minted3. deposit(100,000)shares = 0 after rounding4. transferFrom(victim, vault, 100,000)5. redeem(1)whole balance6. assets out
Ordinary stepAttack step
  1. Attacker (adversary) to QuillonVault: deposit(1). mints 1 share (attack step)
  2. Attacker (adversary) to Deposit asset: transfer(vault, 100,001). no shares minted (attack step)
  3. Depositor (participant) to QuillonVault: deposit(100,000). shares = 0 after rounding (normal step)
  4. QuillonVault (on-chain) to Deposit asset: transferFrom(victim, vault, 100,000) (normal step)
  5. Attacker (adversary) to QuillonVault: redeem(1). whole balance (attack step)
  6. QuillonVault (on-chain) to Attacker: assets out (attack step)
Attacker deposits one base unit, then donates 100,001 units; the next depositor deposits 100,000 units of a 6-decimal asset. Computed from the vault’s conversion formula, rounding down.
ConversionVictim sharesVictim redeemable (units)Victim lossAttacker result (units)
No offset (reviewed commit)00.00100.00%+100,000.00
Virtual offset 10^31,99999,975.490.02%−49,988.24
Virtual offset 10^61,999,98099,999.990.00%−50,000.49

Timeline

  1. Assessment begins on the reviewed commit

  2. Detection

    QV-01 reproduced in a Foundry test and reported privately

  3. Containment

    Deployment of new vaults put on hold

  4. Fix

    Remediation commit with virtual offset and zero-share revert

  5. Verification

    Fix verified; INV-03 holds under fuzzing

  6. Assessment report published

Root cause

Share conversion used the raw supply and asset balance with no offset, and deposit accepted a result of zero shares. The design assumed the first deposit would be large and honest; nothing in the code enforced that.

Two smaller decisions widened the window: assets sent directly to the vault counted toward totalAssets() immediately, and the release script opened each vault in the transaction that deployed it.

Class
First-depositor attack
Severity
Critical
Components involved
QuillonVault
Findings
QV-01QV-05
src/QuillonVault.sol (reviewed commit)solidity
function _convertToShares(uint256 assets, Math.Rounding r) internal view returns (uint256) {    uint256 supply = totalSupply();    return supply == 0 ? assets : assets.mulDiv(supply, totalAssets(), r); // no virtual offset} function deposit(uint256 assets, address receiver) public override returns (uint256 shares) {    shares = previewDeposit(assets);    _deposit(msg.sender, receiver, assets, shares);    // shares may be zero: the deposit still succeeds}

Risk evaluation

At the start of the assessment all findings were open, including QV-01 (critical) and QV-02 (high). The model penalises Code Security for each open finding; Economic Security is reduced while strategy valuation relied on spot reserves.

The after state is the protocol’s current profile in the Quantix Risk Index.

67.8BElevated risk
During the caseB67.8
82.5AModerate risk
Current profileA82.5
CodeSecurityOperationalSecurityGovernanceRiskEconomicSecurityInfrastructureResilienceTransparencyDuring the case, Code Security: 39During the case, Operational Security: 80During the case, Governance Risk: 78During the case, Economic Security: 74During the case, Infrastructure Resilience: 85During the case, Transparency: 88Current profile, Code Security: 84Current profile, Operational Security: 80Current profile, Governance Risk: 78Current profile, Economic Security: 82Current profile, Infrastructure Resilience: 85Current profile, Transparency: 88
During the caseCurrent profile

The score moves from 67.8 (B) to 82.5 (A), a change of +14.7 points. The largest single contribution is Code Security: restoring it from 39.0 to 84.0 alone moves the score from 67.8 to 81.3.

Basis for the case state: All findings open at the start of the assessment; spot-price strategy valuation in place.
ComponentWeightRangeDuring caseTodayScore points
Code Security30%39.084.0+13.5
Operational Security20%80.080.00.0
Governance Risk15%78.078.00.0
Economic Security15%74.082.0+1.2
Infrastructure Resilience10%85.085.00.0
Transparency10%88.088.00.0
Quantix Risk Index100%67.882.5+14.7
During the caseCurrent profileBasis for the case state: All findings open at the start of the assessment; spot-price strategy valuation in place.

Scores use the six weighted components of the Quantix Risk Index. Open findings reduce Code Security by a fixed penalty per severity; the current profile is the protocol’s entry in Quantix Intelligence.

Mitigation strategy

The vault now adds a virtual offset to both sides of the conversion, as in the OpenZeppelin ERC-4626 implementation, and reverts when a deposit or mint would produce zero shares. With an offset of 10^6 an attacker must donate a million times more to cause the same rounding, and most of the donation is captured by the virtual shares.

The release changed as well: each new vault is seeded from the treasury before it opens, and previewWithdraw and previewMint now round up as the standard requires (QV-05).

  1. 01Add _decimalsOffset() = 6 and use the offset formula for both conversions
  2. 02Revert deposits and mints that produce zero shares
  3. 03Round up in previewWithdraw and previewMint
  4. 04Seed every new vault from the treasury before opening it
  5. 05Keep the counterexample as invariant INV-03 in CI
src/QuillonVault.sol (remediation commit)diff
 function _convertToShares(uint256 assets, Math.Rounding r) internal view returns (uint256) {-    uint256 supply = totalSupply();-    return supply == 0 ? assets : assets.mulDiv(supply, totalAssets(), r);+    return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, r); }++function _decimalsOffset() internal pure override returns (uint8) {+    return 6;+}  function deposit(uint256 assets, address receiver) public override returns (uint256 shares) {     shares = previewDeposit(assets);+    if (shares == 0) revert ZeroShares();     _deposit(msg.sender, receiver, assets, shares); }

Security outcome

QV-01 was fixed and verified in the remediation commit before any new vault opened. The invariant that failed during the review holds under fuzzing, and the seeded launch is part of the release checklist.

Together with the QV-02 fix, both ways totalAssets() could be moved by someone who is not depositing are closed.

Detection to containment
45 min
Detection to first fix
11.8 days
Detection to verified fix
14.1 days
Risk Index change
B → A (+14.7)

Lessons learned

  1. 01

    An empty vault is a distinct state with its own threat model; review it explicitly.

  2. 02

    Rounding direction is a security property. A zero-share deposit should never succeed silently.

  3. 03

    Prefer fixes that remove the attacker’s profit, such as virtual offsets, over fixes that only raise the cost.

  4. 04

    Turn every validated exploit into a permanent invariant test.

Have a problem like this one?

Tell us what you are building and where it runs. We will reply with the review plan we would use for your architecture.