Atomic Wool Tuna
High
The failure to segregate externally donated tokens from actual Vault deposits in the share calculation will cause an inflated token balance, resulting in a skewed share price for legitimate Vault depositors as an attacker donates tokens directly to the Strategy and then withdraws shares at an unfair value.
In the Vault contract, the _calcDeposit function calculates shares based on idleBalances
which is the Strategy's current token balances.
/// @notice Calculates the deposit amounts
/// @param bal0 The current token0 balance of the strategy
/// @param bal1 The current token1 balance of the strategy
/// @param depositAmount0 The amount of token0 user wishes to deposit
/// @param depositAmount1 The amount of token1 user wishes to deposit
/// @return shares The amount of shares to be minted to the user
/// @return amount0 The actual amount of token0 which the user will deposit
/// @return amount1 The actual amount of token1 which the user will deposit
function _calcDeposit(uint256 bal0, uint256 bal1, uint256 depositAmount0, uint256 depositAmount1)
internal
view
returns (uint256 shares, uint256 amount0, uint256 amount1)
{
uint256 totalSupply = totalSupply();
// If total supply > 0, vault can't be empty
assert(totalSupply == 0 || bal0 > 0 || bal1 > 0);
if (totalSupply == 0) {
// For first deposit, just use the amounts desired
amount0 = depositAmount0;
amount1 = depositAmount1;
shares = (amount0 > amount1 ? amount0 : amount1) - (1000);
} else if (bal0 == 0) {
amount1 = depositAmount1;
shares = amount1 * totalSupply / bal1;
} else if (bal1 == 0) {
amount0 = depositAmount0;
shares = amount0 * totalSupply / bal0;
} else {
uint256 cross0 = depositAmount0 * bal1;
uint256 cross1 = depositAmount1 * bal0;
uint256 cross = cross0 > cross1 ? cross1 : cross0;
require(cross > 0, "cross");
// Round up amounts
amount0 = (cross - 1) / bal1 + 1;
amount1 = (cross - 1) / bal0 + 1;
shares = cross * totalSupply / bal0 / bal1;
}
}
However, the Strategy does not distinguish between deposited tokens and direct transfers, allowing attackers to artificially inflate balances by donating tokens. This skews the share price, enabling theft from subsequent depositors.
- The attacker needs to make an initial minimal deposit via the Vault to set the baseline share price.
- The Strategy must hold tokens (either deposited or donated) for the attack to manipulate balances.
- The attacker must have access to one of the Vault's underlying tokens (
token0
ortoken1
).
The Vault must have sufficient liquidity for the attacker to withdraw their shares.
- Attacker deposits a minimal amount of both
token0
andtoken1
into the Vault, setting the initial share price. - Attacker donates a large amount of
token0
directly to the Strategy, increasing itstoken0
balance without minting shares. - A legitimate user deposits funds into the Vault. The Vault calculates shares based on the inflated
token0
balance via_calcDeposit
function, minting fewer shares relative to their actual deposit. - The attacker withdraws their shares from the Vault.
- Due to the manipulated share price, the attacker receives a disproportionate amount of the donated token
token0
, effectively stealing funds from the legitimate depositor.
Legitimate Vault depositors suffer a financial loss as their deposit value is diluted by the manipulated share calculation. The attacker gains an unfair advantage by capturing a significant portion of the inflated token balance, potentially resulting in a complete loss of value for the affected deposits if the manipulation is large enough.
Step 1: Attacker makes a minimal deposit to establish share price
Step 2: Attacker donates a large amount of token0
directly to the Strategy
Step 3: A legitimate user makes a deposit, unaware of the manipulated balances
Step 4: Attacker withdraws their shares, receiving an inflated portion of token0
funded by the legitimate user
Introduce a dedicated depositedBalance
variable within the Strategy that increments only when tokens are deposited via the Vault. Use this balance for share calculations instead of idleBalances
uint256 depositedBalance0;
uint256 depositedBalance1;
function deposit(uint256 amount0, uint256 amount1) external onlyVault {
depositedBalance0 += amount0;
depositedBalance1 += amount1;
}
Likewise, it is also viable to prevent or properly account for direct token transfers by overriding the transfer
function in the Strategy.