In the Primary Liquidation, _performForcedBid() is used to create a bid to cover the Erc Debt of the short record being liquidated. If the short order of the record being liquidated still exists and matches with the forced bid, what gets matched is filled into the short record that is currently being liquidated. The problem is that at the end of the liquidation, this short record is canceled in the case of a full liquidation:
Here is a PoC that demonstrates the scenario when a forced bid matches with the short order of the record to be liquidated and a full liquidation occurs:
pragma solidity 0.8.21;
import {Errors} from "contracts/libraries/Errors.sol";
import {Events} from "contracts/libraries/Events.sol";
import {STypes, MTypes, O} from "contracts/libraries/DataTypes.sol";
import {Constants} from "contracts/libraries/Constants.sol";
import {OBFixture} from "test/utils/OBFixture.sol";
import {console} from "forge-std/console.sol";
import "forge-std/Vm.sol";
import {U256, U88, U80} from "contracts/libraries/PRBMathHelper.sol";
import {IAsset} from "interfaces/IAsset.sol";
contract Poc is OBFixture {
using U256 for uint256;
using U88 for uint88;
using U80 for uint80;
function makeShortRecord() public {
MTypes.OrderHint[] memory orderHintArray;
uint16[] memory shortHintArray;
uint80 bidPrice = 0.0030 ether;
uint88 bidErcAmount = 3000 ether;
uint256 bidEth = bidPrice.mul(bidErcAmount);
shortHintArray = setShortHintArray();
orderHintArray = diamond.getHintArray(asset, bidPrice, O.LimitBid);
deal(receiver, bidEth);
vm.startPrank(receiver);
diamond.depositEth{value: bidEth}(address(bridgeReth));
diamond.createBid(asset, bidPrice, bidErcAmount, false, orderHintArray, shortHintArray);
vm.stopPrank();
uint80 price = 0.0021 ether;
uint88 ercAmount = 3000 ether;
uint256 eth = uint256(ercAmount.mul(price) * 5);
orderHintArray = diamond.getHintArray(asset, price, O.LimitShort);
shortHintArray = setShortHintArray();
vm.deal(receiver, eth);
vm.startPrank(receiver);
diamond.depositEth{value: eth}(_bridgeReth);
diamond.createLimitShort(asset, price, ercAmount, orderHintArray, shortHintArray, 500);
vm.stopPrank();
}
function testPOC() public {
makeShortRecord();
MTypes.OrderHint[] memory orderHintArray;
uint16[] memory shortHintArray;
uint80 bidPrice = 0.0021 ether;
uint88 bidErcAmount = 2000 ether;
uint256 bidEth = bidPrice.mul(bidErcAmount);
shortHintArray = setShortHintArray();
orderHintArray = diamond.getHintArray(asset, bidPrice, O.LimitBid);
deal(extra, bidEth);
vm.startPrank(extra);
diamond.depositEth{value: bidEth}(address(bridgeReth));
diamond.createBid(asset, bidPrice, bidErcAmount, false, orderHintArray, shortHintArray);
vm.stopPrank();
uint80 price = 0.0021 ether;
uint88 ercAmount = 4000 ether;
uint256 eth = uint256(ercAmount.mul(price) * 5);
orderHintArray = diamond.getHintArray(asset, price, O.LimitShort);
shortHintArray = setShortHintArray();
vm.deal(sender, eth);
vm.startPrank(sender);
diamond.depositEth{value: eth}(_bridgeReth);
diamond.createLimitShort(asset, price, ercAmount, orderHintArray, shortHintArray, 500);
vm.stopPrank();
int256 newEthPrice = 90000000000000000000;
skipTimeAndSetEth({skipTime: 1 hours, ethPrice: newEthPrice});
vm.startPrank(extra);
diamond.flagShort(asset, sender, Constants.SHORT_STARTING_ID, Constants.HEAD);
vm.stopPrank();
skipTimeAndSetEth({skipTime: 11 hours, ethPrice: newEthPrice});
uint256 ercDebtBeforeLiquidation = diamond.getShortRecord(asset, sender, Constants.SHORT_STARTING_ID).ercDebt;
assert(ercDebtBeforeLiquidation == 2000 ether);
vm.startPrank(extra);
diamond.liquidate(asset, sender, Constants.SHORT_STARTING_ID, shortHintArray);
vm.stopPrank();
uint256 ercDebtAfterLiquidation = diamond.getShortRecord(asset, sender, Constants.SHORT_STARTING_ID).ercDebt;
STypes.ShortRecord[] memory shortRecordsSender = diamond.getShortRecords(asset, sender);
assert(ercDebtAfterLiquidation == 4000 ether);
assert(shortRecordsSender.length == 0);
}
}
There is another instance of this issue in ExitShortFacet.sol in the function exitShort(). Here, a forced bid is also created to settle ERC debt. If it's a full exit, the same issue can occur here as in the liquidation, as the short record is deleted here as well, and it's not taken into consideration that the forced bid may match the short order of the short record.
The ERC debt that arose from the matching with the forced bid and the short order can no longer be repaid as they are no longer present in any short record. Additionally, the collateral from the match with the forced bid cannot be reclaimed by the shorter
The short record being processed should be able to be placed in an editing status. Furthermore, filling out a short record should only be allowed if the short record is not in the editing status.