Pieces Protocol

First Flight #32
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Invalid

[H-1] Potential Reentrancy in TokenDivider::buyOrder function

Summary

The buyOrder function does not adhere to CEI pattern as the Ether transfer to the seller and the contract owner occurs before the ERC20 token transfer, which can lead to reentrancy vulnerabilities.

function buyOrder(uint256 orderIndex, address seller) external payable {
if(seller == address(0)) {
revert TokenDivider__InvalidSeller();
}
SellOrder memory order = s_userToSellOrders[seller][orderIndex];
if(msg.value < order.price) {
revert TokenDivider__IncorrectEtherAmount();
}
uint256 fee = order.price / 100;
uint256 sellerFee = fee / 2;
if(msg.value < order.price + sellerFee) {
revert TokenDivider__InsuficientEtherForFees();
}
balances[msg.sender][order.erc20Address] += order.amount;
s_userToSellOrders[seller][orderIndex] = s_userToSellOrders[seller][s_userToSellOrders[seller].length - 1];
s_userToSellOrders[seller].pop();
emit OrderSelled(msg.sender, order.price);
// Transfer The Ether
(bool success, ) = payable(order.seller).call{value: (order.price - sellerFee)}("");
if(!success) {
revert TokenDivider__TransferFailed();
}
(bool taxSuccess, ) = payable(owner()).call{value: fee}("");
if(!taxSuccess) {
revert TokenDivider__TransferFailed();
}
IERC20(order.erc20Address).transfer(msg.sender, order.amount);
}

Vulnerability Details

This reentrancy in buyOrder could allow the attacker to manipulate the state (e.g., repeat the purchase process) before the function completes its intended execution path

Impact

  • An attacker could repeatedly execute the buyOrder function, draining Ether from the contract or manipulating token balances without proper authorization.

Tools Used

Manual Review

Recommendations

Apply CEI pattern to buyOrder function

function buyOrder(uint256 orderIndex, address seller) external payable {
if (seller == address(0)) {
revert TokenDivider__InvalidSeller();
}
SellOrder memory order = s_userToSellOrders[seller][orderIndex];
if (msg.value < order.price) {
revert TokenDivider__IncorrectEtherAmount();
}
uint256 fee = order.price / 100;
uint256 sellerFee = fee / 2;
if (msg.value < order.price + sellerFee) {
revert TokenDivider__InsuficientEtherForFees();
}
// **Effects**: Update state before making any external calls
balances[msg.sender][order.erc20Address] += order.amount;
// Remove the order from the seller's list
s_userToSellOrders[seller][orderIndex] = s_userToSellOrders[seller][s_userToSellOrders[seller].length - 1];
s_userToSellOrders[seller].pop();
emit OrderSelled(msg.sender, order.price);
// **Interactions**: External calls after state changes
// Transfer ERC20 tokens to the buyer
IERC20(order.erc20Address).transfer(msg.sender, order.amount);
// Transfer Ether to the seller
(bool success, ) = payable(order.seller).call{value: (order.price - sellerFee)}("");
if (!success) {
revert TokenDivider__TransferFailed();
}
// Transfer fee to the contract owner
(bool taxSuccess, ) = payable(owner()).call{value: fee}("");
if (!taxSuccess) {
revert TokenDivider__TransferFailed();
}
}
Updates

Lead Judging Commences

fishy Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

ghufranhassan1 Submitter
5 months ago
ghufranhassan1 Submitter
5 months ago
fishy Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Reentrancy

fishy Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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