Shorters can call ClaimRedemptionFacet::claimRemainingCollateral to claim short.collateral whenever the timeToDispute has passed:
function claimRemainingCollateral(address asset, address redeemer, uint8 claimIndex, uint8 id)
external
isNotFrozen(asset)
nonReentrant
{
STypes.AssetUser storage redeemerAssetUser = s.assetUser[asset][redeemer];
if (redeemerAssetUser.SSTORE2Pointer == address(0)) revert Errors.InvalidRedemption();
// @dev Only need to read up to the position of the SR to be claimed
(, uint32 timeToDispute,,, MTypes.ProposalData[] memory decodedProposalData) =
LibBytes.readProposalData(redeemerAssetUser.SSTORE2Pointer, claimIndex + 1);
>>> if (timeToDispute > LibOrders.getOffsetTime()) revert Errors.TimeToDisputeHasNotElapsed();
MTypes.ProposalData memory claimProposal = decodedProposalData[claimIndex];
if (claimProposal.shorter != msg.sender || claimProposal.shortId != id) revert Errors.CanOnlyClaimYourShort();
STypes.Asset storage Asset = s.asset[asset];
_claimRemainingCollateral({asset: asset, vault: Asset.vault, shorter: msg.sender, shortId: id});
}
However, this process can be bypassed in the following manner:
Redeemer creates a proposal of short records with CR=1.4 and CR=1.7. timeToDispute=3hours
The proposal with CR=1.7 is disputed using a short record of CR=1.6, this results in their removal from the list, leaving a smaller slate ClaimRedemptionFacet#L127 because a portion of the list becomes subject to dispute. 3 hours has passed but the redeemer does not claim the short record CR=1.4.
Another redeemer creates a proposal with short records CR=1.6 and CR=1.7. The short record CR=1.6 can be proposed again because it was disputed in step 2. This new proposal has a new timeToDispute different from the one in step 1.
At this point, the shorter of the short record CR=1.6 from step 3 proposal can call ClaimRedemptionFacet::claimRemainingCollateral without any restriction on the timeToDispute, even when the dispute period has not yet ended.
Furthermore, the CR=1.6 can now be disputed with the SR CR=1.5, and subsequently, the collateral will be returned to the TAPPDisputeRedemptionFacet#L88-L101. This is because the shorter closed the shortRecord in step 4.
The issue occurs in claimRemainingCollateral, where any claimIndex of SSTORE2Pointer can be called without restricting indices that were previously disputed.
function claimRemainingCollateral(address asset, address redeemer, uint8 claimIndex, uint8 id)
external
isNotFrozen(asset)
nonReentrant
{
STypes.AssetUser storage redeemerAssetUser = s.assetUser[asset][redeemer];
if (redeemerAssetUser.SSTORE2Pointer == address(0)) revert Errors.InvalidRedemption();
// @dev Only need to read up to the position of the SR to be claimed
(, uint32 timeToDispute,,, MTypes.ProposalData[] memory decodedProposalData) =
>>> LibBytes.readProposalData(redeemerAssetUser.SSTORE2Pointer, claimIndex + 1);
if (timeToDispute > LibOrders.getOffsetTime()) revert Errors.TimeToDisputeHasNotElapsed();
MTypes.ProposalData memory claimProposal = decodedProposalData[claimIndex];
if (claimProposal.shorter != msg.sender || claimProposal.shortId != id) revert Errors.CanOnlyClaimYourShort();
STypes.Asset storage Asset = s.asset[asset];
_claimRemainingCollateral({asset: asset, vault: Asset.vault, shorter: msg.sender, shortId: id});
}
Proof of Concept
The following test demonstrates how a shorter can call claimRemainingCollateral from a shortRecord that is still within the dispute period:
Implement additional checks in the ClaimRedemptionFacet::claimRemainingCollateral function to ensure that any claims made on a short's collateral are only permissible if all disputes for the relevant short records have been fully resolved.
Lines of code
https://github.com/code-423n4/2024-07-dittoeth/blob/ca3c5bf8e13d0df6a2c1f8a9c66ad95bbad35bce/contracts/facets/ClaimRedemptionFacet.sol#L56 https://github.com/code-423n4/2024-07-dittoeth/blob/ca3c5bf8e13d0df6a2c1f8a9c66ad95bbad35bce/contracts/facets/ClaimRedemptionFacet.sol#L77 https://github.com/code-423n4/2024-07-dittoeth/blob/ca3c5bf8e13d0df6a2c1f8a9c66ad95bbad35bce/contracts/facets/DisputeRedemptionFacet.sol#L127
Vulnerability details
Impact
Shorters can call
ClaimRedemptionFacet::claimRemainingCollateral
to claimshort.collateral
whenever thetimeToDispute
has passed:However, this process can be bypassed in the following manner:
CR=1.4
andCR=1.7
.timeToDispute=3hours
CR=1.7
is disputed using a short record ofCR=1.6
, this results in their removal from the list, leaving a smaller slate ClaimRedemptionFacet#L127 because a portion of the list becomes subject to dispute. 3 hours has passed but the redeemer does not claim the short recordCR=1.4
.CR=1.6
andCR=1.7
. The short recordCR=1.6
can be proposed again because it was disputed instep 2
. This new proposal has a newtimeToDispute
different from the one in step 1.CR=1.6
fromstep 3
proposal can callClaimRedemptionFacet::claimRemainingCollateral
without any restriction on thetimeToDispute
, even when the dispute period has not yet ended.CR=1.6
can now be disputed with the SRCR=1.5
, and subsequently, the collateral will be returned to theTAPP
DisputeRedemptionFacet#L88-L101. This is because the shorter closed theshortRecord
in step 4.The issue occurs in
claimRemainingCollateral
, where anyclaimIndex
ofSSTORE2Pointer
can be called without restricting indices that were previously disputed.Proof of Concept
The following test demonstrates how a shorter can call
claimRemainingCollateral
from ashortRecord
that is still within the dispute period:Tools used
Manual review
Recommended Mitigation Steps
Implement additional checks in the
ClaimRedemptionFacet::claimRemainingCollateral
function to ensure that any claims made on a short's collateral are only permissible if all disputes for the relevant short records have been fully resolved.Assessed type
Invalid Validation