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

Inconsistency in determining value of shares, gives users an inflated almost double the amount when they withdraw

Summary

In PerpetualVault-> the functions mintand handleReturnare relied on for determing the amount of shares to mint to users, and the amount to send to users based on their shares at the time of withdraw, respectively.

The two functions use a different and inconsistent method for determining value: Which can be a design choice, but if it is, it is not executed properly

  • _mintuses total collateral balance at the time of initiating deposit, not including the users deposit in the total collateral balance value

  • _handleReturntries to use the total collateral balance that includes the users withdrawn amount, and not the total collateral balance at the time of initiating the withdraw. There is doubt that it is the design choice to do so, but even if it is the design choice, the calculation is wrong and will lead to user getting a very inflated value transferred to them when the position is open.

For this finding, I will assume it is protocol design decision to use a different method for withdraw, which adds the withdrawn amount to total collateral balance, which is used in determing the value of the shares. And not using the total collateral balance at the time of user initiating the withdraw.

Vulnerability Details

First, lets look at how the _mintfunction determines the amount of shares to mint to the user based on their deposit amount - by using totalAmountBeforewhich effectively uses the balance of the contract at the time of initiating the deposit (not including the newly deposited amount by the user) -> it subtracts the newly deposited amount from the total balance of the contract to achieve this.

** CHECK MY IN LINE COMMENTS for more effective communication

* @param amount actual deposit amount. if `_isLongOneLeverage` is `true`, amount of `indexToken`, or amount of `collateralToken`
*/
function _mint(uint256 depositId, uint256 amount, bool refundFee, MarketPrices memory prices) internal {
uint256 _shares;
if (totalShares == 0) {
_shares = depositInfo[depositId].amount * 1e8;
/*/////////////////////////////////////////////////////*/
// my comments start from here:
} else {
uint256 totalAmountBefore;
if (positionIsClosed == false && _isLongOneLeverage(beenLong)) {
// If the position is closed and long1x Lev
// The contract balance of the index token is used, not collateral
// But still, the deposited amount is deducted from this balance,
// which represent the balance at the time of initiating the deposit
-> totalAmountBefore = IERC20(indexToken).balanceOf(address(this)) - amount;
} else {
// `_totalAmount` returns the total value of the vault in terms of collateral token
// Still here, the users deposited amount is deducted from the total balance
// which represents the balance at the time of initiating the deposit
-> totalAmountBefore = _totalAmount(prices) - amount;
}
if (totalAmountBefore == 0) totalAmountBefore = 1;
// The users share amount which is then minted to them is determined using the
// totalAmountBefore, which in both cases is the balance of the contract,
// at the time of initiating the deposit, not including the users deposit amount
-> _shares = amount * totalShares / totalAmountBefore;
}

The issue is in:

_handleReturn-> which is the end of the user's withdrawal flow, and is used to transfer the determined value amount of the users shares back to the user, and burns their shares. There are 2 different scenarios handled :

  • when the protocol has a closed position

  • when the protocol has an open position

** When the position is closed:

  • calculates amount, using the total collateral balance of the contract (which includes the users withdrawn amount, becasue it was sent in from GMX prior to this when the decrease position was executed)

** When the position is open:

  • the total collateral balance of the contract at the time of the user initiating withdraw is calculated via balanceBeforeWithdraw

    The way and problem with how amountis calculated:

    • 1st: -> becasue of the order of operations of the equation balanceBeforeWithdrawal * shares / totalShareswill be calculated first

    • 2nd: -> withdrawnis added to that calculated value


As a result, the calculated values for amountwhen the position is closed or open IS VERY DIFFERENT

Following the previous methods for determing value used by the protocol -> by using: colalteral balance, shares, and total shares -> the calculated value for amountWILL BE VERY DIFFERENT than what the rest of the protocol produces and expects

** Check inline comments for clearer communication

/**
* @notice this function is an end of withdrawal flow.
* @dev should update all necessary global state variables
*
-> * @param withdrawn amount of token withdrawn from the position
* @param positionClosed true when position is closed completely by withdrawing all funds, or false
*/
function _handleReturn(uint256 withdrawn, bool positionClosed, bool refundFee) internal {
(uint256 depositId) = flowData;
uint256 shares = depositInfo[depositId].shares;
uint256 amount;
if (positionClosed) {
amount = collateralToken.balanceOf(address(this)) * shares / totalShares;
} else {
/*//////////////////////////////////////////////////////*/
// my comments start here:
// baalanceBeforeWithdrawal is calculated which represents the balance of the contract
// at the time the user initiated the withdraw.
// Calculating this value is consistent with `_mint`
-> uint256 balanceBeforeWithdrawal = collateralToken.balanceOf(address(this)) - withdrawn;
// BUT the withdrawn amount is added LAST
// it is not added to the `balanceBeforeWithdrawal`
-> amount = withdrawn + balanceBeforeWithdrawal * shares / totalShares;
}
if (amount > 0) {
_transferToken(depositId, amount);

Impact

For the sake of simplicity, just to illustrate the wrong calculation when the position is open:

  • total collateral = 100,000

  • withdrawn = 20,000

  • shares = 10

  • totalShares = 50

When the position is closed

  • amount = 100,000 * 10 / 50

  • = 20,000

When the position is open

  • balanceBeforeWithdraw= 80,000

  • 20,000 + 80,000 * 10 / 50

  • First the equation will calulate -> 80,000 * 10 / 50

  • = 16,000

  • NEXT it will add 20,000 to that value

  • amount= 20,000 + 16,000

  • amount= 36,000

When the position is open, the user will get their withdrawn amount added to the value of their shares - essentially almost giving the user DOUBLE the amount they should receive.

Tools Used

Manual Review

Recommendations

For the correct calculation in _handleReturn-> the withdrawnamount needs to be added to balanceBeforeWithdrawbefore the rest of the equation continues, to ensure the correct order of operations.

amount = (withdrawn + balanceBeforeWithdrawal) * shares / totalShares;
Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

fresh Submitter
9 months ago
n0kto Lead Judge
8 months ago
n0kto Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!