Summary
The Gamma Vault uses Paraswap and GMX swap functionalities to swap from collateral token to index token and vice versa. Before the GMX swap order is requested, the GmxProxy approve the swap token to GMX exchange router via safeApprove(). If the order fails for some reason, the vault handles it in PerpetualVault.sol::afterOrderCancellation() by setting the nextAction to SWAP_ACTION for the keeper to execute via runNextAction(). This will revert as safeApprove() will revert due to the allowance being non-zero.
Vulnerability Details
Before an order is created in GmxProxy::createOrder(),
function createOrder(
Order.OrderType orderType,
IGmxProxy.OrderData memory orderData
) public returns (bytes32) {
require(msg.sender == perpVault, "invalid caller");
uint256 positionExecutionFee = getExecutionGasLimit(
orderType,
orderData.callbackGasLimit
) * tx.gasprice;
require(
address(this).balance >= positionExecutionFee,
"insufficient eth balance"
);
bytes32 executeOrderFeatureKey = keccak256(
abi.encode(
EXECUTE_ORDER_FEATURE_DISABLED,
orderHandler,
orderType
)
);
require(
dataStore.getBool(executeOrderFeatureKey) == false,
"gmx execution disabled"
);
gExchangeRouter.sendWnt{value: positionExecutionFee}(
orderVault,
positionExecutionFee
);
if (
orderType == Order.OrderType.MarketSwap ||
orderType == Order.OrderType.MarketIncrease
) {
@> IERC20(orderData.initialCollateralToken).safeApprove(
address(gmxRouter),
orderData.amountIn
);
....
This is fine when orders are successfully, as the allowance is set back to 0. But when a swap fails, the approval transactions still go through while the proxy wait for GMX to callback. If an order is cancelled for some reason, GMX will call afterOrderCancellation(), which will call PerpetualVault.sol::afterOrderCancellation().
function afterOrderCancellation(
bytes32 key,
Order.Props memory order,
EventLogData memory
) external override validCallback(key, order) {
IGmxProxy.OrderResultData memory orderResultData = IGmxProxy
.OrderResultData(
order.numbers.orderType,
order.flags.isLong,
order.numbers.sizeDeltaUsd,
address(0),
0,
queue.isSettle
);
IPerpetualVault(perpVault).afterOrderCancellation(
key,
order.numbers.orderType,
orderResultData
);
delete queue;
}
In PerpetualVault.sol::afterOrderCancellation(), OrderType is MarketSwap and NextActionSelector will be SWAP_ACTION.
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) {
nextAction.selector = NextActionSelector.SETTLE_ACTION;
} else if (orderType == Order.OrderType.MarketSwap) {
@> nextAction.selector = NextActionSelector.SWAP_ACTION;
nextAction.data = abi.encode(swapProgressData.remaining, swapProgressData.isCollateralToIndex);
} else {
....
This do a GMX swap or a Paraswap or both based on the keeper's input in runNextAction(). If it makes a GMX swap via runSwap() -> _doGmxSwap(), GmxProxy.sol::createOrder() will revert due to safeApprove().
function safeApprove(IERC20 token, address spender, uint256 value) internal {
require(
@> (value == 0) || (token.allowance(address(this), spender) == 0),
"SafeERC20: approve from non-zero to non-zero allowance"
);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
Impact
The Vault cant retry GMX swaps if they fail.
Tools Used
Manual Review
Recommendations
Reset safeApprove to 0 after order creation.