Vulnerability Details
When combining short orders, the ids
array is not checked for duplication nor is the current status of the first short checked right before the merge or inside the merge function itself.
function combineShorts(address asset, uint8[] memory ids)
external
isNotFrozen(asset)
nonReentrant
onlyValidShortRecord(asset, msg.sender, ids[0])
{
for (uint256 i = ids.length - 1; i > 0; i--) {
uint8 _id = ids[i];
_onlyValidShortRecord(_asset, msg.sender, _id);
firstShort.merge(ercDebt, ercDebtSocialized, collateral, yield, c.shortUpdatedAt);
https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/ShortRecordFacet.sol#L117C14-L176
Hence the firstId
could repeat in the ids
array, which will cause the firstShort
to be marked cancelled inside the loop but still proceed to merge with the firstShort
.
POC Test
@@ -378,6 +378,39 @@ contract ShortsTest is OBFixture {
);
}
+ function test_CombiningReapeatedFirstLeadsToLoss() public {
+ fundLimitBidOpt(SHORT1_PRICE, DEFAULT_AMOUNT, receiver);
+ fundLimitShortOpt(SHORT1_PRICE, DEFAULT_AMOUNT, sender);
+ STypes.ShortRecord memory shortRecord1BeforeCombine =
+ getShortRecord(sender, Constants.SHORT_STARTING_ID);
+
+ fundLimitBidOpt(SHORT1_PRICE, DEFAULT_AMOUNT, receiver);
+ fundLimitShortOpt(SHORT1_PRICE, DEFAULT_AMOUNT, sender);
+ STypes.ShortRecord memory shortRecord2BeforeCombine =
+ getShortRecord(sender, Constants.SHORT_STARTING_ID + 1);
+
+ //providing the 1st short twice in the array
+ vm.prank(sender);
+ uint8[] memory shortRecords = new uint8[](3);
+ shortRecords[0] = shortRecord1BeforeCombine.id;
+ shortRecords[1] = shortRecord1BeforeCombine.id;
+ shortRecords[2] = shortRecord2BeforeCombine.id;
+ diamond.combineShorts(asset, shortRecords);
+
+ STypes.ShortRecord memory shortRecord1AfterCombine =
+ getShortRecord(sender, shortRecord1BeforeCombine.id);
+ STypes.ShortRecord memory shortRecord2AfterCombine =
+ getShortRecord(sender, shortRecord2BeforeCombine.id);
+
+ //the short records are cancelled and the entire collateral is in the cancelled shortRecord1
+ assertEq(shortRecord1AfterCombine.status == SR.Cancelled, true);
+ assertEq(shortRecord2AfterCombine.status == SR.Cancelled, true);
+ assertEq(
+ shortRecord1AfterCombine.collateral,
+ shortRecord1BeforeCombine.collateral + shortRecord1BeforeCombine.collateral + shortRecord2BeforeCombine.collateral
+ );
+ }
+
//////Combine Shorts//////
function testCombineShortsx2() public {
makeShorts();
Impact
Loss of user's collateral
Recommendations
Validate if the firstId repeats in the array or check the status of the firstShort just before merge.
@@ -6,7 +6,7 @@ import {U256, U80, U88} from "contracts/libraries/PRBMathHelper.sol";
import {Errors} from "contracts/libraries/Errors.sol";
import {Events} from "contracts/libraries/Events.sol";
import {Modifiers} from "contracts/libraries/AppStorage.sol";
-import {STypes, MTypes} from "contracts/libraries/DataTypes.sol";
+import {STypes, SR, MTypes} from "contracts/libraries/DataTypes.sol";
import {LibAsset} from "contracts/libraries/LibAsset.sol";
import {LibShortRecord} from "contracts/libraries/LibShortRecord.sol";
import {LibOracle} from "contracts/libraries/LibOracle.sol";
@@ -172,6 +172,10 @@ contract ShortRecordFacet is Modifiers {
LibShortRecord.deleteShortRecord(_asset, msg.sender, _id);
}
+ //check for cancellation
+ if (firstShort.status == SR.Cancelled) {
+ revert("Duplicate First Short Id");
+ }
// Merge all short records into the short at position id[0]
firstShort.merge(ercDebt, ercDebtSocialized, collateral, yield, c.shortUpdatedAt);