Pieces Protocol

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

Reentrancy Vulnerability in buyOrder() function

Relevant GitHub Links

https://github.com/Cyfrin/2025-01-pieces-protocol/blob/4ef5e96fced27334f2a62e388a8a377f97a7f8cb/src/TokenDivider.sol#L109-L137

Summary

The buyOrder function in TokenDivider contract contains a critical reentrancy vulnerability that allows an attacker to repeatedly purchase the same order before the state is updated.

Vulnerability Details

The vulnerability exists because the contract:

  • Performs erc20 transfer before updating state

  • Lacks reentrancy protection

  • Use low level call which can be exploited

function buyOrder(uint256 orderIndex, address seller) external payable {
// ... state checks ...
(bool success, ) = payable(order.seller).call{value: (order.price - sellerFee)}("");
// State changes occur after external call
s_userToSellOrders[seller][orderIndex] = s_userToSellOrders[seller][s_userToSellOrders[seller].length - 1];
s_userToSellOrders[seller].pop();
}

Impact

An attacker can:

  • Purchase same order multiple times

  • Drain contract funds

  • Get multiple ERC20 tokens for single payment

POC

contract ReentrancyTest is Test {
TokenDivider divider;
address attacker = makeAddr("attacker");
function testReentrancy() public {
vm.deal(attacker, 2 ether);
vm.startPrank(attacker);
// Setup order
// ... order creation code ...
// Attack
divider.buyOrder{value: 1.02 ether}(0, seller); // Price + fee
// Verify multiple purchases occurred
assertTrue(attackSucceeded);
}
}
contract AttackContract {
TokenDivider target;
bool attacked;
receive() external payable {
if(!attacked) {
attacked = true;
target.buyOrder{value: msg.value}(0, msg.sender);
}
}
}

Tools Used

Mannul Review

Recommendations

CEI

function buyOrder(uint256 orderIndex, address seller) external payable nonReentrant {
// ... checks ...
// Effects
s_userToSellOrders[seller][orderIndex] = s_userToSellOrders[seller][s_userToSellOrders[seller].length - 1];
s_userToSellOrders[seller].pop();
// Interactions
(bool success, ) = payable(order.seller).call{value: (order.price - sellerFee)}("");
}
Updates

Lead Judging Commences

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

Support

FAQs

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