Tadle

Tadle
DeFiFoundry
27,750 USDC
View results
Submission Details
Severity: high
Valid

Loss of Collateral Tokens Due to Incorrect State Update in `PreMarkets::createTaker`

Summary

Changes made to originOfferInfo are lost because they are incorrectly stored in memory, causing a loss of collateral tokens for protocol users when the original makers of an AskOffer in turbo mode settle.

Vulnerability Details

In PreMarkets::listOffer, the function updates the abortOfferStatus of the originOfferInfo to establish the connection between the newly created offer and its parent offer. However, the state update is incorrectly implemented.

if (makerInfo.offerSettleType == OfferSettleType.Turbo) {
address originOffer = makerInfo.originOffer;
@> OfferInfo memory originOfferInfo = offerInfoMap[originOffer];
if (_collateralRate != originOfferInfo.collateralRate) {
revert InvalidCollateralRate();
}
@> originOfferInfo.abortOfferStatus = AbortOfferStatus.SubOfferListed;
}

As shown in the code snippet above, the originOfferInfo is being updated in memory, meaning the change is lost once the function finishes execution. As a result, originOfferInfo.abortOfferStatus remains set to Initialized (default status).

enum AbortOfferStatus {
Initialized, // @audit uint8 default value: 0
SubOfferListed,
Aborted
}

The Issue:
The abortOfferStatus is used in the validation process to allow or disallow Ask makers from aborting their offers in PreMarkets::abortAskOffer. Failing to properly update the originOfferInfo.abortOfferStatus will cause a loss of funds for the protocol when a sub-offer that has sold its points is aborted. The taker who buys the points can then abort their stock. Takers are not allowed to abort their stock unless their settling maker has aborted their offer, as seen below.

@> if (preOfferInfo.abortOfferStatus != AbortOfferStatus.Aborted) {
revert InvalidAbortOfferStatus(
AbortOfferStatus.Aborted,
preOfferInfo.abortOfferStatus
);
}

This constraint is in place to avoid a scenario where a taker aborts their stock and retrieves the initial deposit, while the associated maker still pays point tokens for their greater-than-zero used points.

Proof of Concept

Scenario: I know this isn't the most straightforward concept to explain, but follow me here.

Action 1: User_A calls PreMarkets::createOffer to create an Ask Offer for 1000 points in turbo mode.

  • User_A's offerInfo.points: 1000

  • User_A's offerInfo.usedPoints: 0 // No points sold yet

  • User_A's offerInfo.abortOfferStatus: AbortOfferStatus.Initialized

Action 2: User_B calls PreMarkets::createTaker to buy 500 points from User_A and later calls PreMarkets::listOffer to sell these 500 points

  • User_A's offerInfo.points: 1000

  • User_A's offerInfo.usedPoints: 500 // Points sold to User_B

  • User_A's offerInfo.abortOfferStatus: AbortOfferStatus.Initialized // The issue: remains Initialized instead of changing to SubOfferListed

  • User_B's offerInfo.points: 500 // Points bought from User_A

  • User_B's offerInfo.usedPoints: 0 // No points sold yet

  • User_B's offerInfo.abortOfferStatus: AbortOfferStatus.Initialized // Newly listed stocks are always set to Initialized

Action 3: User_C calls PreMarkets::createTaker to buy 500 points from User_B

Note: User_C only has a stock as they haven't listed their points yet.

  • User_A's offerInfo.points: 1000

  • User_A's offerInfo.usedPoints: 500 // Points sold to User_B

  • User_A's offerInfo.abortOfferStatus: AbortOfferStatus.Initialized // The issue: remains Initialized instead of changing to SubOfferListed

  • User_B's offerInfo.points: 500 // Points bought from User_A

  • User_B's offerInfo.usedPoints: 500 // Points sold to User_C

  • User_B's offerInfo.abortOfferStatus: AbortOfferStatus.Initialized // Still Initialized as this is not the original offer

Action 4: User_B calls PreMarkets::abortAskOffer to abort their offer

Note: The protocol lets them abort their offer, as turbo makers are not obligated to settle anyone.

  • User_B's offerInfo.points: 500 // Points bought from User_A

  • User_B's offerInfo.usedPoints: 500 // Points sold to User_C

  • User_B's offerInfo.abortOfferStatus: AbortOfferStatus.Aborted // AbortOfferStatus updates correctly

Action 5: User_C calls PreMarkets::abortBidTaker to abort their stock

  • Ideally, the protocol should not allow this abortion because the original offerInfo.usedPoints are still > 0, and the associated maker is obligated to settle User_C.

if (preOfferInfo.abortOfferStatus != AbortOfferStatus.Aborted) {
revert InvalidAbortOfferStatus(
AbortOfferStatus.Aborted,
preOfferInfo.abortOfferStatus
);
}
  • Settling Ask makers with offerInfo.usedPoints greater than zero are always charged point tokens and/or collateral tokens when settling.

  • The problem here is that User_C exits the trade, retrieving their initial deposit, while the maker of the original offer is still obligated to deposit point tokens to access their collateral.

tokenManager.addTokenBalance(
TokenBalanceType.SalesRevenue,
_msgSender(),
makerInfo.tokenAddress,
makerRefundAmount
);

Impact

This leads to a loss of tokens for the protocol. The final withdrawal of the collateral type from the protocol bears the brunt of the loss.

Tools Used

Manual

Recommendations

Update the originOfferInfo in storage.

if (makerInfo.offerSettleType == OfferSettleType.Turbo) {
address originOffer = makerInfo.originOffer;
- OfferInfo memory originOfferInfo = offerInfoMap[originOffer];
+ OfferInfo storage originOfferInfo = offerInfoMap[originOffer];
if (_collateralRate != originOfferInfo.collateralRate) {
revert InvalidCollateralRate();
}
originOfferInfo.abortOfferStatus = AbortOfferStatus.SubOfferListed;
}
Updates

Lead Judging Commences

0xnevi Lead Judge about 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-PreMarkets-listOffer-originIOfferInfo-storage-memory

Valid high severity, because the `abortOfferStatus` of the offer is not updated and persist through `storage` when listing an offer for turbo mode within the `offerInfoMap` mapping, it allows premature abortion given the `abortOfferStatus` defaults to `Initialized`, allowing the bypass of this [check](https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/PreMarkets.sol#L552-L557) here and allow complete refund of initial collateral + stealing of trade tax which can potentially be gamed for profits using multiple addresses

Support

FAQs

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