Pieces Protocol

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

TokenDivider::buyOrder Function Vulnerable to Front-Running Attacks

Summary

TokenDivider::buyOrder Function Vulnerable to Front-Running Attacks

Vulnerability Details

The TokenDivider::buyOrder function is vulnerable to front-running attacks. When a buyer submits a transaction to purchase tokens at a certain price, a malicious actor can monitor the mempool and front-run the transaction by submitting their own purchase with a higher gas price, effectively stealing the buying opportunity from the original buyer.

Impact

This vulnerability can result in:

  • Buyers being unable to secure their desired orders

  • Malicious actors profiting by front-running legitimate transactions

  • Loss of user confidence in the platform

  • Potential market manipulation through systematic front-running

Proof of Concept

Add the following test to the TokenDividerTest.t.sol contract to demonstrate the front-running vulnerability:

function testFrontRunningVulnerability() public nftDivided {
address attacker = makeAddr("attacker");
uint256 orderIndex = 0;
ERC20Mock erc20Mock = ERC20Mock(
tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address
);
vm.startPrank(USER);
erc20Mock.approve(address(tokenDivider), AMOUNT);
tokenDivider.sellErc20(address(erc721Mock), 1 ether, AMOUNT);
vm.stopPrank();
vm.deal(USER2, 2 ether);
vm.startPrank(attacker);
vm.deal(attacker, 2 ether);
tokenDivider.buyOrder{value: 1.1 ether}(orderIndex, USER);
vm.stopPrank();
vm.prank(USER2);
vm.expectRevert();
tokenDivider.buyOrder{value: 1 ether}(orderIndex, USER);
assertEq(tokenDivider.getBalanceOf(attacker, address(erc20Mock)), AMOUNT);
assertEq(tokenDivider.getBalanceOf(USER2, address(erc20Mock)), 0);
}

Tools Used

Foundry

Recommendations

Implement a commit-reveal scheme or use a batch auction mechanism or use private mempools to avoid getting front-run:

+ struct Commitment {
+ bytes32 hash;
+ uint256 timestamp;
+ bool revealed;
+ }
+ mapping(address => Commitment) public commitments;
+ uint256 public constant COMMITMENT_DEADLINE = 10 minutes;
+ function commitToBuy(bytes32 commitment) external {
+ commitments[msg.sender] = Commitment({
+ hash: commitment,
+ timestamp: block.timestamp,
+ revealed: false
+ });
+ }
function buyOrder(
+ bytes32 nonce,
uint256 orderIndex,
address seller
) external payable {
+ Commitment storage userCommitment = commitments[msg.sender];
+
+ require(userCommitment.timestamp + COMMITMENT_DEADLINE > block.timestamp,
+ "Commitment expired");
+
+ require(!userCommitment.revealed, "Already revealed");
+
+ require(keccak256(abi.encodePacked(msg.sender, nonce)) == userCommitment.hash,
+ "Invalid commitment");
+
+ userCommitment.revealed = true;
// Rest of the existing buyOrder logic
}
Updates

Lead Judging Commences

fishy Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Front-running

Support

FAQs

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