DittoETH

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

Users cannot re-flag a short between the 16th & 17th hour

Summary

As per docs and code comments one can re-flag a short once 16 hours have passed since it was first flagged but not yet liquidated.
Due to a logic error in flagShort(), users have to wait till the completion of 17 hours, to be able to flag such a short.

Vulnerability Details & Impact

L60 calculates adjustedTimestamp as:

File: contracts/facets/MarginCallPrimaryFacet.sol
60 @> uint256 adjustedTimestamp = LibOrders.getOffsetTimeHours();

which is then compared to resetLiquidationTime on L64-L67:

File: contracts/facets/MarginCallPrimaryFacet.sol
64 uint256 timeDiff = adjustedTimestamp - short.updatedAt;
65 uint256 resetLiquidationTime = LibAsset.resetLiquidationTime(asset);
66
67 @> if (timeDiff <= resetLiquidationTime) {
68 revert Errors.MarginCallAlreadyFlagged();
69 }

LibOrders.getOffsetTimeHours() returns:

File: contracts/libraries/LibOrders.sol
30 function getOffsetTimeHours() internal view returns (uint24 timeInHours) {
31 @> return uint24(getOffsetTime() / 1 hours);
32 }

Any timestamp is rounded-down to the hour. So 16 hours 30 minutes would be considered as 16 hours and hence not greater than resetLiquidationTime.

Important to note that even the updatedAt value of an order is being set using LibOrders.getOffsetTimeHours() instead of using an exact timestamp, as can be seen here.

Let's see the problem this poses with an example:

Assume that the "starting time" or contract deployment time is at midnight i.e 00:00 .
Suppose short gets updated at timestamp: 5:05 . Protocol will store this in rounded-down form as `updatedAt` = 5:00
Suppose the protocol allows a user to exit (or flag, or some action) this short after `X` hours. Let X = 2.
So the user should ideally be able to exit after 7:05.
However, when he tries, at 7:06, his `adjustedTimestamp` is calculated (rounded-down) as 7:00. The `timeDiff` = 7:00 - 5:00 = 2 hours.
This is less than equal to X (or `resetLiquidationTime` or some deadline) i.e `timeDiff <= X`, so he won't be allowed to perform the action.
The user will have to wait another 54 minutes till 8:00 so that timeDiff is 3 hours.

Impact

Users cannot re-flag a short between the 16th & 17th hour. Shorters gets an extra hour to correct their cRatio.

PoC

Create a file inside test/ folder by the name of IncorrectReFlag.t.sol and paste the following code. It has the test along with a few helper functions:

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.21;
import {Constants} from "contracts/libraries/Constants.sol";
import {Errors} from "contracts/libraries/Errors.sol";
import {U256, U88} from "contracts/libraries/PRBMathHelper.sol";
import {OBFixture} from "test/utils/OBFixture.sol";
import {STypes, SR} from "contracts/libraries/DataTypes.sol";
contract IncorrectReFlag is OBFixture {
using U256 for uint256;
using U88 for uint88;
function _initialAssertStruct() public {
r.ethEscrowed = 0;
r.ercEscrowed = DEFAULT_AMOUNT;
assertStruct(receiver, r);
s.ethEscrowed = 0;
s.ercEscrowed = 0;
assertStruct(sender, s);
e.ethEscrowed = 0;
e.ercEscrowed = 0;
assertStruct(extra, e);
t.ethEscrowed = 0;
t.ercEscrowed = 0;
assertStruct(tapp, t);
}
function _prepareAsk(uint80 askPrice, uint88 askAmount) public {
fundLimitBidOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, receiver);
fundLimitShortOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, sender);
fundLimitAskOpt(askPrice, askAmount, receiver);
assertEq(getShortRecordCount(sender), 1);
assertEq(
getShortRecord(sender, Constants.SHORT_STARTING_ID).collateral,
DEFAULT_AMOUNT.mulU88(DEFAULT_PRICE) * 6
);
_initialAssertStruct();
}
function _checkFlaggerAndUpdatedAt(
address _shorter,
uint16 _flaggerId,
uint256 _updatedAt
) public {
STypes.ShortRecord memory shortRecord =
getShortRecord(_shorter, Constants.SHORT_STARTING_ID);
assertEq(shortRecord.flaggerId, _flaggerId, "flagger id mismatch");
assertEq(shortRecord.updatedAt, _updatedAt, "updatedAt mismatch");
}
function _flagShortAndSkipTime(uint256 timeToSkip, address flagger) public {
_prepareAsk({askPrice: DEFAULT_PRICE, askAmount: DEFAULT_AMOUNT});
_setETH(2666 ether);
_checkFlaggerAndUpdatedAt({_shorter: sender, _flaggerId: 0, _updatedAt: 0});
vm.prank(flagger);
diamond.flagShort(asset, sender, Constants.SHORT_STARTING_ID, Constants.HEAD);
uint256 flagged = diamond.getOffsetTimeHours();
skipTimeAndSetEth({skipTime: timeToSkip, ethPrice: 2666 ether});
_checkFlaggerAndUpdatedAt({_shorter: sender, _flaggerId: 1, _updatedAt: flagged});
assertSR(
getShortRecord(sender, Constants.SHORT_STARTING_ID).status, SR.FullyFilled
);
depositEth(tapp, DEFAULT_TAPP);
}
function test_CantflagShortBetween16To17Hours() public {
address _flagger_01 = makeAddr("_flagger_01");
address _flagger_02 = makeAddr("_flagger_02");
_flagShortAndSkipTime({timeToSkip: 16 hours + 30 minutes, flagger: _flagger_01});
// Case 1: Incorrectly reverts even though
// 16 hours have passed; anyone should be have been able to flag it again
vm.prank(_flagger_02);
vm.expectRevert(Errors.MarginCallAlreadyFlagged.selector);
diamond.flagShort(asset, sender, Constants.SHORT_STARTING_ID, Constants.HEAD);
rewind(16 hours + 30 minutes);
// Case 2: Allows flagging, but only after 17 hours
skip(17 hours);
vm.prank(_flagger_02);
diamond.flagShort(asset, sender, Constants.SHORT_STARTING_ID, Constants.HEAD);
}
}

Run forge test --mt test_CantflagShortBetween16To17Hours -vv to verify the impact.

The test passes, but should have failed as per protocol specifications.

Tools Used

Manual review, foundry.

Recommendations

Instead of doing these calculations in hours, they should be done in seconds to avoid rounding error.

Updates

Lead Judging Commences

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

finding-569

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

finding-569

Support

FAQs

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