DeFiFoundry
50,000 USDC
View results
Submission Details
Severity: low
Valid

Locked funds due to overflow via shares decimal scaling

Summary

Liquidations inflate the share decimals to dilute existing holders. Over successive liquidations, this exponential share inflation can trigger arithmetic overflows during withdrawal calculations, locking funds and potentially causing significant losses for users.

Vulnerability Details

When shares are first minted to depositors, they use 14 decimals (6 decimals for USDC * 1e8):

However, the number of decimals can increase when a liquidation occurs.

Let's say there are 100,000e14 shares and there's an open 2x leverage long position. A user deposits 1000e6 USDC to the position, but before runNextAction() is called by a keeper, a liquidation occurs liquidating the entire position.

Gamma doesn't burn the 100,000e14 shares, but instead dilutes them to them point where they are essentially worthless. This is done in the shares calculation.

First, we calculate the totalAmountBefore, which is the total value of the vault prior to the deposit. Since the position is liquidated this will be 0.

Then we multiple the amount deposited by the total number of shares and then divide by totalAmountBefore to determine how many shares to mint.

To avoid a divide by 0 error, Gamma updates 0 to 1:

function _mint(uint256 depositId, uint256 amount, bool refundFee, MarketPrices memory prices) internal {
uint256 _shares;
if (totalShares == 0) {
_shares = depositInfo[depositId].amount * 1e8;
} else {
uint256 totalAmountBefore;
if (positionIsClosed == false && _isLongOneLeverage(beenLong)) {
totalAmountBefore = IERC20(indexToken).balanceOf(address(this)) - amount;
} else {
@> totalAmountBefore = _totalAmount(prices) - amount;
}
@> if (totalAmountBefore == 0) totalAmountBefore = 1;
@> _shares = amount * totalShares / totalAmountBefore;
}
depositInfo[depositId].shares = _shares;
totalShares = totalShares + _shares;
...SNIP...
}

Now, when we mint the depositor their shares, they receive 1000e6 * 100,000e14 / 1 shares = 1000e25.

Since the prior 100,000e14 shareholders position was liquidated, their shares are only now worth ~.0000001% of the position.

This seems to work, but a big problem arrises the more the vault gets liquidated.

Let's say the next time a liquidation occurs, the vault has 100,000e25 shares. The next depositor would receive 1000e6 * 100,000e25 = 1000e36 shares

Since shares are a uint256, the maximum value is 2^256 - 1 or approximately ~1.157e77.

It's reasonable to assume that each time a liquidation occurs, the decimals on shares get scaled up somewhere between 9 and 11 decimal places, which means that after ~6 liquidations, we'd be approaching the max value of a uint256.

This poses huge problems when a user attempts to withdraw their funds. When \_withdraw is called there are multiple places (denoted below) where an overflow could occur because shares are multiplied by something:

function _withdraw(uint256 depositId, bytes memory metadata, MarketPrices memory prices) internal {
uint256 shares = depositInfo[depositId].shares;
if (shares == 0) {
revert Error.ZeroValue();
}
if (positionIsClosed) {
_handleReturn(0, true, false);
} else if (_isLongOneLeverage(beenLong)) { // beenLong && leverage == BASIS_POINTS_DIVISOR
uint256 swapAmount = IERC20(indexToken).balanceOf(address(this)) * shares / totalShares;
nextAction.selector = NextActionSelector.SWAP_ACTION;
nextAction.data = abi.encode(swapAmount, false);
} else if (curPositionKey == bytes32(0)) {
_handleReturn(0, true, false);
} else {
IVaultReader.PositionData memory positionData = vaultReader.getPositionInfo(curPositionKey, prices);
@> uint256 collateralDeltaAmount = positionData.collateralAmount * shares / totalShares;
@> uint256 sizeDeltaInUsd = positionData.sizeInUsd * shares / totalShares;
...SNIP...
}
}

The overflow would lead to funds becoming stuck for withdrawers.

It's important to point out that this overflow can occur if there's less than 6 liquidations depending on how large the value shares is being multiplied against. For example, sizeDeltaInUSD uses 30 decimals, so if shares approach 77-30 = 47 decimals, this overflow is a risk and would only take ~3 liquidations.

Impact

Loss of funds.

Tools Used

Manual review.

Recommendations

Consider potentially burning the shares of users when a position is liquidated so decimals don't scale.

Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_full_liquidation_do_not_reset_totalShares

Likelihood: Low/Medium, when fully liquidated. Liquidation often returns some tokens and shares are important to withdraw them. Moreover, shares are inflated, so only little part of tokens with huge value (WBTC) will be impacted. Impact: High, Previous depositor is able to withdraw token from the new depositors if the value of the token is huge like for WBTC.

Appeal created

sakshamseth5 Auditor
9 months ago
kkk Auditor
9 months ago
wellbyt3 Submitter
9 months ago
riceee Auditor
9 months ago
codexbugmenot Auditor
9 months ago
n0kto Lead Judge
8 months ago
n0kto Lead Judge 8 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_totalAmountBefore_is_1_incorrect_calculation

Likelihood: Extremely low, full liquidation several times, and the admin should unpause several times the vault. But since keeper is the real component to check during a liquidation, it's still valid. Impact: High, any new deposit once the above conditions are met, will inflates shares and DoS the vault.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!