This vulnerability arises because the vault’s calculation of its total collateral relies directly on the ERC20 token balance (using collateralToken.balanceOf(address(this))
) when no active position exists. In effect, any user even one acting without malicious intent, can send additional collateral tokens directly to the vault without triggering the deposit logic. This extra balance gets included in share‐minting and withdrawal calculations, thereby inflating the apparent pool value. As a consequence, a user who deposits via the proper channel can later withdraw more than what they originally contributed, upsetting the fairness of the system.
The issue directly conflicts with the protocol’s core invariants. For example, one key invariant is that the value of a depositor’s shares should only reflect what was deposited, ensuring that no user gains an unfair advantage due to the actions of another. When the contract uses the entire token balance to compute the pool value, it ignores the fact that not every token in the vault has come through the deposit mechanism. This misalignment between external token transfers and internal accounting can allow a user to game the system. The consequence is a dilution of other users’ share value, as the extra tokens skew the share allocation formula during deposits and withdrawals.
It undermines the reliability and fairness of the protocol. Given the complexity of the system in which automated strategies and dynamic share minting are central to its operation, this vulnerability presents a high-level risk that must be addressed to maintain trust and correctness.
The flaw stems from the following implementation in the vault’s _totalAmount
function:
When the vault is not engaged in an active position, it simply returns the ERC20 balance of the collateral token. This design does not differentiate between funds deposited via the vault’s controlled interface and tokens that are sent directly to the contract address.
This becomes problematic during the share minting process. Consider the following snippet from the _mint
function:
Here, if a user deposits an amount that is later used to compute new shares, the denominator (totalAmountBefore
) is based on the entire on‑chain balance. Should an external party send additional collateral tokens directly to the vault, this balance becomes artificially inflated. When a withdrawal occurs, the computed return (which is proportional to the vault’s total balance) will then exceed the genuine deposit.
For instance, imagine the following scenario:
A user deposits 1,000 tokens via the proper deposit mechanism. The vault mints shares based on an uninflated pool.
Separately, another user (or even a well‐meaning party unaware of the internal accounting) transfers an extra 5,000 tokens directly to the vault.
The vault’s balance is now 6,000 tokens, but its internal record still only reflects the 1,000 deposited via the deposit() function.
Upon withdrawal, the user’s share calculation is based on a 6,000-token balance, effectively letting them redeem a value greater than their original contribution.
Here's a proof of concept demonstrating the issue:
Manual Review
Foundry
To address this vulnerability, it is essential to decouple the vault’s notion of total collateral from the raw ERC20 token balance. One viable solution is to maintain an internal ledger that accurately tracks only those tokens deposited via the vault’s controlled interface. For example, the contract should use an internal variable (such as totalDepositAmount
) for share calculations rather than relying on collateralToken.balanceOf(address(this))
.
Specifically, the _totalAmount
function should be revised so that when the position is closed it returns the sum of the tracked deposits, not the entire balance. Any direct transfers should be either rejected or properly segregated so they do not affect share calculations. Additionally, the deposit function could perform a pre- and post-balance check to ensure that only the intended tokens are counted as part of the deposit. This change will preserve the invariant that a depositor’s share value reflects only the funds that were intentionally contributed via the vault’s deposit mechanism, thereby preventing gaming of the system.
Prevent tokens from getting stuck. This is a simple donation to every shareholder, and therefore, every share has more value (profit), leading to fewer shares for future users. If this is not intended by the user, it is merely a mistake! For totalAmountBefore > amount * totalShares, Here is the worst-case scenario (with the minimum amount): first deposit 0.001000 (e4) USDC → 1e12 shares. Second deposit: 0.001 (e4) USDC * 1e12 / 0.001 (e4) USDC. So, the attacker would need to donate 1e16 tokens, meaning 1e10 USDC → 10 billion $. Even in the worst case, it's informational.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.