Patch Thursday — Retrospecting Arbitrary Position Cancellation Vulnerability in Perpetual Protocol

ChainLight
ChainLight Blog & Research
4 min readOct 19, 2023

Summary

ChainLight identified and reported a vulnerability in the _cancelExcessOrders() function of Perpetual Protocol’s ClearingHouse contract in July 2022. The vulnerability stemmed from a lack of proper authorization validation for canceling positions. This vulnerability occurred because the function that cancels the orders of makers (_cancelExcessOrders()) did not verify whether the order passed as an argument belongs to the maker.

Vulnerability Detail

The ClearingHouse contract of Perpetual Protocol manages the settlement and liquidation of positions. Among the functions in the contract, _cancelExcessOrders() is implemented to allow everyone to cancel the open positions (orders) of the maker who satisfies the liquidation condition. In order to cancel the orders, users pass the on-chain address of the maker, the base token of the position, and the ID of the orders. The ID of an order, orderId, is calculated through keccak256(abi.encodePacked(maker, baseToken, lowerTick, UpperTick).

The code below shows cancelExcessOrder() function of ClearingHouse contract. If the selected maker satisfies the condition for the liquidation, anyone can call _cancelExcessOrders() with arbitrary orderId and cancel the order. In this process, attackers can carry out an attack by canceling arbitrary orders, even if they are not the target of liquidation, as there is no verification on whether the order corresponding to the specified orderId belongs to the maker.

/// @inheritdoc IClearingHouse
function cancelExcessOrders(
address maker,
address baseToken,
bytes32[] calldata orderIds
) external override whenNotPaused nonReentrant {
// input requirement checks:
// maker: in _cancelExcessOrders()
// baseToken: in Exchange.settleFunding()
// orderIds: in OrderBook.removeLiquidityByIds()
_checkMarketOpen(baseToken);
_cancelExcessOrders(maker, baseToken, orderIds);
}

Also, the code below shows the operation of _cancelExcessOrders(), which is called by cancelExcessOrders(). At the bottom of the code, when removing the maker's liquidity after cancellation, it utilizes order.liquidity, which implies the function is intended to remove the entire liquidity of the order. However, if the attackers exploit the previously mentioned vulnerability, they could partially remove the liquidity of victims. This situation arises when the attackers use malicious orderId other than those belonging to the orders of the victims. This unintended behavior in the protocol could lead to other vulnerabilities.

/// @dev only cancel open orders if there are not enough free collateral with mmRatio
/// or account is able to being liquidated.
function _cancelExcessOrders(
address maker,
address baseToken,
bytes32[] memory orderIds
) internal {
if (orderIds.length == 0) {
return;
}
// CH_NEXO: not excess orders
require(
(_getFreeCollateralByRatio(maker, IClearingHouseConfig(_clearingHouseConfig).getMmRatio()) < 0) ||
_isLiquidatable(maker),
"CH_NEXO"
);
// must settle funding first
_settleFunding(maker, baseToken);
IOrderBook.RemoveLiquidityResponse memory removeLiquidityResponse;
uint256 length = orderIds.length;
for (uint256 i = 0; i < length; i++) {
OpenOrder.Info memory order = IOrderBook(_orderBook).getOpenOrderById(orderIds[i]);
IOrderBook.RemoveLiquidityResponse memory response =
_removeLiquidity(
IOrderBook.RemoveLiquidityParams({
maker: maker,
baseToken: baseToken,
lowerTick: order.lowerTick,
upperTick: order.upperTick,
liquidity: order.liquidity // !! here !!
})
);
...
}

Impact

This attack occurs during the liquidation process, allowing the attacker to reduce the position of the victim by an arbitrary amount. Attackers can provide liquidity (open positions) as desired and use the resulting hash value as the orderId, and remove the liquidity of victims by the amount they supply.

Furthermore, invalid orders may be calculated or visualized in the cancelExcessOrder dashboard, significantly reducing the functionality of the dashboard.

Lastly, considering that the _cancelExcessOrders() function was implemented with the purpose of removing the entire liquidity of the position, the vulnerability that allows partial removal of the liquidity introduces the possibility of other unintended side effects.

Recommended Solution

The protocol needs to add code for the verification of whether the given orderId belongs to the maker before removing liquidity through the _removeLiquidity() function. An example would be:

require(OpenOrder.calcOrderKey(maker, baseToken, order.lowerTick, order.upperTick) == orderIds[i], "CH_DMID"); // CH_DMID: the maker of the orderId is diffrent with the given user.

Proof of Concept

// % forge test --fork-url <https://mainnet.optimism.io> --fork-block-number 15786920 --chain-id 10 --etherscan-api-key YourApiKeyToken -vv
[PASS] test() (gas: 8480571)
Logs:
initial accountValue (1): 13878416573031000000000000
initial accountValue (2): 200000000000000000000000
initial accountValue (3): 13878416573031000000000000
initial accountValue (total): 27956833146062000000000000
initial accountValue (total in USDC): 27956833.146062
currentTick: 4659
oraclePrice: 159700000
setupLargePositions()
maker: 0x185a4dc360ce69bdccee33b3784b0282f7961aea
v_flow: 0x7eada83e15acd08d22ad85a1dce92e5a257acb92
targetPoc2LowerTick: 4980
targetPoc2UpperTick: 5040
tick: 4680
tick: 4739

simulatePriceMove()
targetTick: 4502
tick: 4710
tick: 4680
tick: 4646
tick: 4612
tick: 4578
tick: 4544
tick: 4510
tick: 4476
oraclePrice: 151715000

cancelExcessOrderByUsingMaliciousOrderIds()
Original accounts[1]'s orderid: 0xaef991f988a9ef792ab7c1545172b2f12935c6041643219faa40496423e6ddc3
otherMaliciousOrderId from accounts[0]: 0x2596a205fe48888af0239495c94a98e40d93df76bd7a97271f889887dd18ad7d
*** Oh yeah, a malicious attacker use different user's orderhash to remove the partial of victim's liquidity! ****

✨ We are ChainLight!

ChainLight explores new and effective blockchain security technologies with rich practical experience and deep technical understanding. Our innovative security audits built upon such research proactively identify and eliminate various security risks and vulnerabilities in the Web3 ecosystem. To ensure continuous security even after the audit, we provide a digital asset risk management solution using on-chain data monitoring and automated vulnerability detection services.

ChainLight serves to guide and protect all users of decentralized services, lighting the way for a safer Web3 ecosystem.

  • Want to see more from the ChainLight team? 👉 Check out our Twitter account.

🌐 Website: chainlight.io | 📩 TG: @chainlight | 📧 chainlight@theori.io

--

--

ChainLight
ChainLight Blog & Research

Established in 2016, ChainLight's award-winning experts provide tailored security solutions to fortify your smart contract and help you thrive on the blockchain