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

Incorrect Share Minting Logic

Summary

The PerpetualVault contract mints shares for depositors based on incomplete valuation of the vault's assets. When a position is open (leveraged or spot), the calculation does not fully account for the current market value of the position, leading to unfair share distribution.

https://github.com/CodeHawks-Contests/2025-02-gamma/blob/e5b98627a4c965e203dbb616a5f43ec194e7631a/contracts/PerpetualVault.sol#L762

Vulnerability Details

Faulty Code Snippet:

// PerpetualVault.sol
function _mint(...) 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; // ⚠️ Incorrect NAV
}
_shares = (amount * totalShares) / totalAmountBefore;
}
// ...
}

Incomplete Valuation: When the vault has an open position, _totalAmount(prices) includes the position's value. However, subtracting the new deposit (amount) from it ignores the proportional impact of the new funds on the position’s value.

Spot Position Handling: For 1x leverage (spot), the code uses the raw indexToken balance instead of its USD value, leading to incorrect valuations if the token’s price changes.

Example Scenario

Assumptions:

  • Existing Vault Value:

100k(includes50k collateral + $50k open position profit).

New Deposit: $50k.

Faulty Calculation:

totalAmountBefore = _totalAmount(prices) - amount = ($100k) - $50k = $50k;
_shares = ($50k * 100 shares) / $50k = 100 shares;

Result:

New depositor gets 100 shares for 50k,but the true pre−deposit value was 100k. Their fair share should be 50 shares (50k/100k * 100 shares).

Impact

  1. Dilution: New depositors receive more shares than deserved, diluting existing shareholders.

  2. Losses for Existing Users: Profits from open positions are not factored into share price, allowing new users to claim a disproportionate share of gains.

Tools Used

Manual review

Recommendations

Shares must be minted based on the Net Asset Value (NAV) of the vault, which includes:

  • Collateral balance.

  • Current value of open positions (mark-to-market).

  • Pending fees or rebates.

function _mint(uint256 depositId, uint256 amount, MarketPrices memory prices) internal {
uint256 navBefore = _calculateNAV(prices); // Includes collateral + position value
uint256 _shares;
if (totalShares == 0) {
_shares = amount * 1e8; // Initial shares
} else {
_shares = (amount * totalShares) / navBefore; // Shares proportional to NAV
}
totalShares += _shares;
depositInfo[depositId].shares = _shares;
}
function _calculateNAV(MarketPrices memory prices) internal view returns (uint256) {
uint256 collateralBalance = collateralToken.balanceOf(address(this));
uint256 positionValue = vaultReader.getPositionInfo(curPositionKey, prices).netValue;
return collateralBalance + positionValue;
}

Why the fix works:

Accurate NAV Calculation:

Use _calculateNAV to get the true value of collateral + open positions.

Fetch position value from vaultReader using current market prices.

Proportional Shares:

New shares = (depositAmount * totalShares) / NAVBeforeDeposit.

Verification

Test Case 1 (Open Position with Profit):

NAV before deposit: $100k (existing shares = 100).

Deposit: $50k.

New shares = ($50k * 100) / $100k = 50 (correct ✅).

Test Case 2 (Spot Position, Price Increase):

indexToken balance: 10 ETH (price:

2k→20k).

Deposit: $10k.

NAV before deposit: $20k (existing shares = 100).

New shares = ($10k * 100) / $20k = 50 (correct ✅).

Updates

Lead Judging Commences

n0kto Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Suppositions

There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.

Support

FAQs

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