The sellErc20 function lacks input validation for the price parameter, allowing users to create sell orders with zero price. This leads to fee calculation issues and enables free token acquisitions.
**Lines of code**
https://github.com/Cyfrin/2025-01-pieces-protocol/blob/4ef5e96fced27334f2a62e388a8a377f97a7f8cb/src/TokenDivider.sol#L221
```solidity
//Vulnerable Code Section
function sellErc20(address nftPegged, uint256 price, uint256 amount) external {
// ❌ No validation for price == 0
if(nftPegged == address(0)) {
revert TokenDivider__NftAddressIsZero();
}
if( amount == 0) {
revert TokenDivider__AmountCantBeZero();
}
// ... rest of function
}
```
**In buyOrder function:**
https://github.com/Cyfrin/2025-01-pieces-protocol/blob/4ef5e96fced27334f2a62e388a8a377f97a7f8cb/src/TokenDivider.sol#L274
```solidity
uint256 fee = order.price / 100; // ❌ Zero price leads to zero fees
```
## Vulnerability Details
Here's the sequence that demonstrates the problem:
**User lists tokens for sale with zero price:**
```solidity
divider.sellErc20(nftAddress, 0, amount);
```
**Buyer can purchase tokens for free:**
**Calculations in buyOrder:**
```solidity
fee = price/100 = 0/100 = 0
sellerFee = fee/2 = 0/2 = 0
//price + sellerFee = 0 + 0 = 0
if(msg.value < order.price + sellerFee ) {
revert TokenDivider__InsuficientEtherForFees();
}
```
## Coded POC
- 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 TokenDividerTest is Test {
TokenDivider public tokenDivider;
ERC721Mock public mockNFT;
address public owner;
address public user1;
address public user2;
uint256 constant NFT_ID = 0; //first nft id will be 0
uint256 constant INITIAL_AMOUNT = 1000;
function setUp() public {
owner = makeAddr("owner");
user1 = makeAddr("user1");
user2 = makeAddr("user2");
vm.startPrank(owner);
tokenDivider = new TokenDivider();
mockNFT = new ERC721Mock();
vm.stopPrank();
vm.startPrank(owner);
mockNFT.mint(user1); // minted tokenId 0 to user1
vm.stopPrank();
}
function testZeroPriceExploit() public {
// Step 1: User1 divides their NFT into fractions
vm.startPrank(user1);
mockNFT.approve(address(tokenDivider), 0);
tokenDivider.divideNft(address(mockNFT), 0, INITIAL_AMOUNT);
// Get the ERC20 token address
TokenDivider.ERC20Info memory info = tokenDivider.getErc20InfoFromNft(address(mockNFT));
address erc20Address = info.erc20Address;
// Approve tokens for selling
IERC20(erc20Address).approve(address(tokenDivider), INITIAL_AMOUNT);
// Step 2: Create a sell order with price = 0
uint256 sellAmount = 500;
uint256 price = 0; // Zero price!
tokenDivider.sellErc20(address(mockNFT), price, sellAmount);
vm.stopPrank();
// Step 3: User2 buys the tokens
vm.startPrank(user2);
// sent 0 ETH to buy the tokens
// Fee calculation: fee = price/100 = 0/100 = 0
// sellerFee = fee/2 = 0/2 = 0
// Total required = price + sellerFee = 0 + 0 = 0
tokenDivider.buyOrder{value: 0}(0, user1);
// Verify user2 received the tokens
uint256 user2Balance = tokenDivider.getBalanceOf(user2, erc20Address);
assertEq(user2Balance, sellAmount, "User2 should receive the tokens");
// Verify no ETH was transferred (since price and fees are 0)
assertEq(user1.balance, 0, "Seller should receive no ETH");
assertEq(owner.balance, 0, "Owner should receive no fees");
vm.stopPrank();
}
}
```
## Impact:
**Free Token Acquisition:** Buyers can obtain tokens without payment
**Lost Protocol Fees:** Platform receives no fees *(fee = price/100 = 0)*
**Lost Seller Revenue:** Sellers receive no payment *(price - sellerFee = 0 - 0 = 0)*
**Economic Implications:**
- Undermines the entire trading mechanism
- Protocol loses revenue from fees
- Creates unfair market dynamics
## Tools Used
Manual Review
## Fix Recommendation:
Add price validation in the sellErc20 function:
```solidity
// Correct Implementation
function sellErc20(address nftPegged, uint256 price, uint256 amount) external {
if(price == 0) {
revert TokenDivider__InvalidPrice();
}
if(nftPegged == address(0)) {
revert TokenDivider__NftAddressIsZero();
}
// ... rest of the function
}
```
The fix ensures that all sell orders must have a non-zero price, maintaining the economic integrity of the platform and ensuring proper fee collection.