Tadle

Tadle
DeFi
30,000 USDC
View results
Submission Details
Severity: low
Invalid

Reentrancy Vulnerability in closeBidOffer and closeBidTaker Functions

Summary

While reviewing the DeliveryPlace.sol contract, I identified a reentrancy vulnerability within the closeBidOffer and closeBidTaker functions. These functions are designed to close bid offers and takers, respectively, but the way they interact with external contracts leaves them exposed to potential reentrancy attacks. Specifically, the functions make external calls to other contracts before updating internal state variables or emitting events, which could allow an attacker to exploit the contract and cause significant damage.

Vulnerability Details

in closeBidOffer(address _offer) Function

Vulnerable Code:

function closeBidOffer(address _offer) external {
(
OfferInfo memory offerInfo,
MakerInfo memory makerInfo,
,
MarketPlaceStatus status
) = getOfferInfo(_offer);
if (_msgSender() != offerInfo.authority) {
revert Errors.Unauthorized();
}
if (offerInfo.offerType == OfferType.Ask) {
revert InvalidOfferType(OfferType.Bid, OfferType.Ask);
}
if (
status != MarketPlaceStatus.AskSettling &&
status != MarketPlaceStatus.BidSettling
) {
revert InvaildMarketPlaceStatus();
}
if (offerInfo.offerStatus != OfferStatus.Virgin) {
revert InvalidOfferStatus();
}
uint256 refundAmount = OfferLibraries.getRefundAmount(
offerInfo.offerType,
offerInfo.amount,
offerInfo.points,
offerInfo.usedPoints,
offerInfo.collateralRate
);
// Vulnerable External Call
ITokenManager tokenManager = tadleFactory.getTokenManager();
tokenManager.addTokenBalance(
TokenBalanceType.MakerRefund,
_msgSender(),
makerInfo.tokenAddress,
refundAmount
);
// Another Vulnerable External Call
IPerMarkets perMarkets = tadleFactory.getPerMarkets();
perMarkets.updateOfferStatus(_offer, OfferStatus.Settled);
// Event Emission After External Calls
emit CloseBidOffer(
makerInfo.marketPlace,
offerInfo.maker,
_offer,
_msgSender()
);
}

The closeBidOffer function is used to close a bid offer, calculate the refund amount based on the offer’s parameters, and then interact with external contracts to update token balances and offer status.

tokenManager.addTokenBalance(
TokenBalanceType.MakerRefund,
_msgSender(),
makerInfo.tokenAddress,
refundAmount
);

This is an external call to the TokenManager contract. The contract's state is not fully updated before this call, leaving the contract in a potentially inconsistent state that an attacker could exploit by re-entering the function.

perMarkets.updateOfferStatus(_offer, OfferStatus.Settled);

This is another external call, this time to the PerMarkets contract. Again, the state of the contract is not fully updated before this call is made

emit CloseBidOffer(
makerInfo.marketPlace,
offerInfo.maker,
_offer,
_msgSender()
);

The event is emitted after the external calls. Best practice is to emit events before making external calls to reduce the reentrancy attack surface.

Attacker Could Exploit This

Step 1: The attacker could create a malicious contract that interacts with this function. During the execution of closeBidOffer, when tokenManager.addTokenBalance is called, the attacker's contract could use a fallback function to re-enter closeBidOffer.

Step 2: By re-entering the function, the attacker could trigger another refund before the status is updated to Settled, potentially causing multiple refunds or other unintended state changes.

Step 3: This manipulation could lead to draining the contract of funds or leaving the contract in an inconsistent state, affecting the integrity of the marketplace.

in closeBidTaker(address _stock) Function

function closeBidTaker(address _stock) external {
IPerMarkets perMarkets = tadleFactory.getPerMarkets();
ITokenManager tokenManager = tadleFactory.getTokenManager();
StockInfo memory stockInfo = perMarkets.getStockInfo(_stock);
if (stockInfo.preOffer == address(0x0)) {
revert InvalidStock();
}
if (stockInfo.stockType == StockType.Ask) {
revert InvalidStockType();
}
if (_msgSender() != stockInfo.authority) {
revert Errors.Unauthorized();
}
(
OfferInfo memory preOfferInfo,
MakerInfo memory makerInfo,
,
) = getOfferInfo(stockInfo.preOffer);
OfferInfo memory offerInfo;
uint256 userRemainingPoints;
if (makerInfo.offerSettleType == OfferSettleType.Protected) {
offerInfo = preOfferInfo;
userRemainingPoints = stockInfo.points;
} else {
offerInfo = perMarkets.getOfferInfo(makerInfo.originOffer);
if (stockInfo.offer == address(0x0)) {
userRemainingPoints = stockInfo.points;
} else {
OfferInfo memory listOfferInfo = perMarkets.getOfferInfo(
stockInfo.offer
);
userRemainingPoints =
listOfferInfo.points -
listOfferInfo.usedPoints;
}
}
if (userRemainingPoints == 0) {
revert InsufficientRemainingPoints();
}
if (offerInfo.offerStatus != OfferStatus.Settled) {
revert InvalidOfferStatus();
}
if (stockInfo.stockStatus != StockStatus.Initialized) {
revert InvalidStockStatus();
}
uint256 collateralFee;
if (offerInfo.usedPoints > offerInfo.settledPoints) {
if (offerInfo.offerStatus == OfferStatus.Virgin) {
collateralFee = OfferLibraries.getDepositAmount(
offerInfo.offerType,
offerInfo.collateralRate,
offerInfo.amount,
true,
Math.Rounding.Floor
);
} else {
uint256 usedAmount = offerInfo.amount.mulDiv(
offerInfo.usedPoints,
offerInfo.points,
Math.Rounding.Floor
);
collateralFee = OfferLibraries.getDepositAmount(
offerInfo.offerType,
offerInfo.collateralRate,
usedAmount,
true,
Math.Rounding.Floor
);
}
}
uint256 userCollateralFee = collateralFee.mulDiv(
userRemainingPoints,
offerInfo.usedPoints,
Math.Rounding.Floor
);
// Vulnerable External Call
tokenManager.addTokenBalance(
TokenBalanceType.RemainingCash,
_msgSender(),
makerInfo.tokenAddress,
userCollateralFee
);
uint256 pointTokenAmount = offerInfo.settledPointTokenAmount.mulDiv(
userRemainingPoints,
offerInfo.usedPoints,
Math.Rounding.Floor
);
// Another Vulnerable External Call
tokenManager.addTokenBalance(
TokenBalanceType.PointToken,
_msgSender(),
makerInfo.tokenAddress,
pointTokenAmount
);
// Vulnerable External Call
perMarkets.updateStockStatus(_stock, StockStatus.Finished);
// Event Emission After External Calls
emit CloseBidTaker(
makerInfo.marketPlace,
offerInfo.maker,
_stock,
_msgSender(),
userCollateralFee,
pointTokenAmount
);
}

The closeBidTaker function is designed to finalize a bid taker, calculate any collateral fees, and update token balances and stock status accordingly.

tokenManager.addTokenBalance(
TokenBalanceType.RemainingCash,
_msgSender(),
makerInfo.tokenAddress,
userCollateralFee
);

This is an external call to TokenManager before the function’s internal state is fully updated. This leaves the contract exposed to reentrancy.

tokenManager.addTokenBalance(
TokenBalanceType.PointToken,
_msgSender(),
makerInfo.tokenAddress,
pointTokenAmount
);

This second external call happens before the contract's internal state has been finalized and before any event is emitted, leaving the contract vulnerable to reentrancy.

perMarkets.updateStockStatus(_stock, StockStatus.Finished);

Yet another external call made before the event is emitted. This sequence further exposes the contract to a potential reentrancy attack.

emit CloseBidTaker(
makerInfo.marketPlace,
offerInfo.maker,
_stock,
_msgSender(),
userCollateralFee,
pointTokenAmount
);

The event is emitted after all external calls, which is a less secure practice and increases the vulnerability to reentrancy attacks.

Attacker Could Exploit This:

Step 1: The attacker could deploy a contract that interacts with closeBidTaker. When the function calls tokenManager.addTokenBalance, the attacker’s contract could trigger a fallback function to re-enter closeBidTaker.

Step 2: By re-entering, the attacker could manipulate the state to issue multiple payments or other unintended actions before the internal state is finalized.

Step 3: This can lead to unauthorized fund withdrawals, inconsistent stock statuses, or multiple unintended token transfers.

Impact

The primary impact of these vulnerabilities is that they allow an attacker to perform reentrancy attacks, which could lead to the contract being drained of funds or left in an inconsistent state. Such exploits could cause significant financial losses and compromise the integrity of the entire marketplace.

Tools Used

Manual Code Review

Recommendations

Implement ReentrancyGuard:
Use OpenZeppelin’s ReentrancyGuard in these functions to prevent reentrancy attacks. This will ensure that once a function is entered, it cannot be re-entered until it completes.

Example Implementation

function closeBidOffer(address _offer) external nonReentrant {
// Function logic
}
function closeBidTaker(address _stock) external nonReentrant {
// Function logic
}

Reorder Function Logic:
Reorder the function code so that all state changes and event emissions occur before any external calls. This practice minimizes the risk of reentrancy attacks.

Example Fix for closeBidOffer:

// First, update internal state and emit events
perMarkets.updateOfferStatus(_offer, OfferStatus.Settled);
emit CloseBidOffer(
makerInfo.marketPlace,
offerInfo.maker,
_offer,
_msgSender()
);
// Then, make external calls
tokenManager.addTokenBalance(
TokenBalanceType.MakerRefund,
_msgSender(),
makerInfo.tokenAddress,
refundAmount
);
Updates

Lead Judging Commences

0xnevi Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality
Assigned finding tags:

[invalid] finding-PreMarkets-reentrancy

Invalid, all [vague generalities](https://docs.codehawks.com/hawks-auditors/how-to-determine-a-finding-validity#vague-generalities) talking about possible reentrancies 11and afaik, reentrancy is not possible and not proven.

Support

FAQs

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