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

In the Contract another user will be sent a refund instead of the User withdrawing when their Order is processed successfully

Summary

Whenever a User call to withdraw his token his desposit id is saved but this id is not used during execution fee refund calling this user to lose the msg.value passed in.

Vulnerability Details

There is a bug in the burn function but the fix there doesn't solve this issue as it still presists.

  1. In a 1x Contract for example Alice calls to withdraw 1000 USD with 16 USD execution fee

  2. We call the _withdraw function or we set and wait for keeper to call

  3. After funds are processed the call back calls to finalize all actions

  4. For a 1x leverage used as an example we set a SWAP action see flow below

*/
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;
@audit>> nextAction.selector = NextActionSelector.SWAP_ACTION; // we no spend na note
// abi.encode(swapAmount, swapDirection): if swap direction is true, swap collateralToken to indexToken
@audit>> nextAction.data = abi.encode(swapAmount, false);
}

5 . SWAP action

} else if (_nextAction.selector == NextActionSelector.SWAP_ACTION) {
(, bool isCollateralToIndex) = abi.decode(
_nextAction.data,
(uint256, bool)
);
_runSwap(metadata, isCollateralToIndex, prices);

6 . runswap calls to line 1007 after swaping

} else {
if (metadata.length != 1) {
revert Error.InvalidData();
}
(PROTOCOL _protocol, bytes memory data) = abi.decode(metadata[0], (PROTOCOL, bytes));
if (_protocol == PROTOCOL.DEX) {
uint256 outputAmount = _doDexSwap(data, isCollateralToIndex);
// update global state
if (flow == FLOW.DEPOSIT) {
// last `depositId` equals with `counter` because another deposit is not allowed before previous deposit is completely processed
_mint(counter, outputAmount + swapProgressData.swapped, true, prices); // Note refunded for just 1x leverage contract.
@audit>> } else if (flow == FLOW.WITHDRAW) {
@audit>> _handleReturn(outputAmount + swapProgressData.swapped, false, true); // another refund if 1x leverage //bug REFUND FOR SWAP // IF NA DECREASE ORDE WE DIDNOT
} else {

7 . This then tries to refund Alice his 16 USD since their was no call to Gmx

/**
* @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 {
@audit>>> (uint256 depositId) = flowData;
uint256 shares = depositInfo[depositId].shares;
uint256 amount;
if (positionClosed) {
amount = collateralToken.balanceOf(address(this)) * shares / totalShares;
} else {
uint256 balanceBeforeWithdrawal = collateralToken.balanceOf(address(this)) - withdrawn;
amount = withdrawn + balanceBeforeWithdrawal * shares / totalShares;
}
if (amount > 0) {
_transferToken(depositId, amount);
}
emit Burned(depositId, depositInfo[depositId].recipient, depositInfo[depositId].shares, amount);
_burn(depositId); // burning before refund // loss of fee to refund // high
@audit>>> if (refundFee) { //bug // occurs majorly in a 1x trading system........note
uint256 usedFee = callbackGasLimit * tx.gasprice;
@audit>>> if (depositInfo[depositId].executionFee > usedFee) {
@audit>>> try IGmxProxy(gmxProxy).refundExecutionFee(depositInfo[counter].owner,
@audit>>> depositInfo[counter].executionFee - usedFee) {} catch {} // fuck bug use deposit id not counter.......... high 3.
}
}

8 . From using deposit id we suddenly change to counter which is the last depositor not the present User.

Bob is the last user he will get a refund.

Alice id is 1 not counter although get the 1000 USD, alice will lose her refund 16 USD that was not used.

Impact

Loss of the refunded fee sent into the contract for the user calling withdrawal.

Tools Used

Manual Review

Recommendations

Use the deposit id and not counter to process refund.

Updates

Lead Judging Commences

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

finding_counter_invalid_during_handleReturn

Likelihood: Medium/High, when withdraw on a 1x vault. Impact: High, the fees will be distributed to the last depositor and not the withdrawer.

Support

FAQs

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

Give us feedback!