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

The new position can be created without closing previous position.

Summary

When keeper changes position type, PerpetualVault.sol#afterOrderCancellation() function does not delete next action.
So keeper script can run next action. Then, new position of changed type is created even though previous position is not closed.

Vulnerability Details

We say that PerpetualVault.sol#run() function is called by keeper to change position type.

function run(
bool isOpen,
bool isLong,
MarketPrices memory prices,
bytes[] memory metadata
) external nonReentrant {
_noneFlow();
_onlyKeeper();
if (gmxProxy.lowerThanMinEth()) {
revert Error.LowerThanMinEth();
}
flow = FLOW.SIGNAL_CHANGE;
if (isOpen) {
if (positionIsClosed) {
...
} else {
if (beenLong == isLong) {
revert Error.NoAction();
} else {
// Close current position first and then open the requested position in the next action
@> nextAction.selector = NextActionSelector.INCREASE_ACTION;
@> nextAction.data = abi.encode(isLong);
if (_isLongOneLeverage(beenLong)) {
_runSwap(metadata, false, prices);
} else {
(uint256 acceptablePrice) = abi.decode(metadata[0], (uint256));
@> _createDecreasePosition(0, 0, beenLong, acceptablePrice, prices);
}
}
}
} else {
...
}
}

As we can see above, next action is queued and then it creates order.
Here, we say that _isLongOneLeverage(beenLong) = false.
But order is canceled by some errors, then PerpetualVault.sol#afterOrderCancellation() function is called.

function afterOrderCancellation(
bytes32 requestKey,
Order.OrderType orderType,
IGmxProxy.OrderResultData memory orderResultData
) external {
if (msg.sender != address(gmxProxy)) {
revert Error.InvalidCall();
}
_gmxLock = false;
if (orderResultData.isSettle) {
// Retry settle action.
nextAction.selector = NextActionSelector.SETTLE_ACTION;
} else if (orderType == Order.OrderType.MarketSwap) {
// If GMX swap fails, retry in the next action.
nextAction.selector = NextActionSelector.SWAP_ACTION;
// abi.encode(swapAmount, swapDirection): if swap direction is true, swap collateralToken to indexToken
nextAction.data = abi.encode(swapProgressData.remaining, swapProgressData.isCollateralToIndex);
} else {
if (flow == FLOW.DEPOSIT) {
nextAction.selector = NextActionSelector.INCREASE_ACTION;
nextAction.data = abi.encode(beenLong);
} else if (flow == FLOW.WITHDRAW) {
nextAction.selector = NextActionSelector.WITHDRAW_ACTION;
} else {
@> // If signal change fails, the offchain script starts again from the current status.
@> delete flowData;
@> delete flow;
}
}
emit GmxPositionCallbackCalled(requestKey, false);
}

Here, it deletes flowData and flow because orderType = MarketDecrease and flow = SIGNAL_CHANGE.
But it does not delete nextAction.
The nextAction remaines to INCREASE_ACTION.

Then, keeper script calls runNextAction().

function runNextAction(MarketPrices memory prices, bytes[] memory metadata) external nonReentrant gmxLock {
_onlyKeeper();
Action memory _nextAction = nextAction;
delete nextAction;
if (_nextAction.selector == NextActionSelector.INCREASE_ACTION) {
(bool _isLong) = abi.decode(_nextAction.data, (bool));
if (_isLongOneLeverage(_isLong)) {
_runSwap(metadata, true, prices);
} else {
// swap indexToken that could be generated from the last action into collateralToken
// use only DexSwap
if (
IERC20(indexToken).balanceOf(address(this)) * prices.indexTokenPrice.min >= ONE_USD
) {
(, bytes memory data) = abi.decode(metadata[1], (PROTOCOL, bytes));
_doDexSwap(data, false);
}
(uint256 acceptablePrice) = abi.decode(metadata[0], (uint256));
@> _createIncreasePosition(_isLong, acceptablePrice, prices);
}
} ...
}

So new position is created even though previous position is not closed.

Impact

The protocol is broken from new created position without closing previous position.

Tools Used

Manual Review

Recommendations

Modify PerpetualVault.sol#afterOrderCancellation() function as follows.

function afterOrderCancellation(
bytes32 requestKey,
Order.OrderType orderType,
IGmxProxy.OrderResultData memory orderResultData
) external {
...
if (orderResultData.isSettle) {
...
} else if (orderType == Order.OrderType.MarketSwap) {
...
} else {
if (flow == FLOW.DEPOSIT) {
nextAction.selector = NextActionSelector.INCREASE_ACTION;
nextAction.data = abi.encode(beenLong);
} else if (flow == FLOW.WITHDRAW) {
nextAction.selector = NextActionSelector.WITHDRAW_ACTION;
} else {
// If signal change fails, the offchain script starts again from the current status.
delete flowData;
delete flow;
++ delete nextAction;
}
}
emit GmxPositionCallbackCalled(requestKey, false);
}
Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

invalid_afterOrderCancellation_do_not_reset_nextAction

Normal behavior, the keeper will retry to increase the position. That’s why there is this condition in `_createIncreasePosition`: ``` if (flow == FLOW.DEPOSIT) { amountIn = depositInfo[counter].amount; flowData = vaultReader.getPositionSizeInTokens(curPositionKey); } else { amountIn = collateralToken.balanceOf(address(this)); } ```

Support

FAQs

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

Give us feedback!