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
}