pragma solidity ^0.8.34;
import "forge-std/Test.sol";
import "../src/NFTDealers.sol";
contract PriceTruncationTest is Test {
NFTDealers public nftDealers;
address public owner = address(0x1);
address public seller = address(0x2);
address public buyer = address(0x3);
address public usdcHolder = address(0x4);
IERC20 public usdc;
uint256 constant LOCK_AMOUNT = 20 * 10**6;
uint256 constant MAX_UINT32 = 4_294_967_295;
uint256 public constant PRICE_5K_USDC = 5_000 * 10**6;
uint256 public constant PRICE_10K_USDC = 10_000 * 10**6;
uint256 public constant PRICE_50K_USDC = 50_000 * 10**6;
uint256 public constant PRICE_100K_USDC = 100_000 * 10**6;
uint256 public constant PRICE_1M_USDC = 1_000_000 * 10**6;
event Debug(string message, uint256 value);
function setUp() public {
usdc = IERC20(address(new ERC20Mock("USDC", "USDC", 6)));
nftDealers = new NFTDealers(
owner,
address(usdc),
"Test Collection",
"TEST",
"ipfs://image/",
LOCK_AMOUNT
);
vm.startPrank(owner);
nftDealers.whitelistWallet(seller);
nftDealers.revealCollection();
vm.stopPrank();
deal(address(usdc), seller, 1_000_000 * 10**6);
vm.startPrank(seller);
usdc.approve(address(nftDealers), LOCK_AMOUNT);
nftDealers.mintNft();
vm.stopPrank();
}
function testPriceTruncation() public {
console.log("=== Price Truncation Demonstration ===");
console.log("uint32 max value:", MAX_UINT32);
console.log("uint32 max USDC (with 6 decimals):", MAX_UINT32 / 10**6, "USDC\n");
console.log("Test 1: Price within uint32 range (5,000 USDC)");
testListingPrice(PRICE_5K_USDC, "5,000 USDC");
console.log("\nTest 2: Price above uint32 max (50,000 USDC)");
testListingPrice(PRICE_50K_USDC, "50,000 USDC");
console.log("\nTest 3: Price far above uint32 max (100,000 USDC)");
testListingPrice(PRICE_100K_USDC, "100,000 USDC");
console.log("\nTest 4: Extreme price (1,000,000 USDC)");
testListingPrice(PRICE_1M_USDC, "1,000,000 USDC");
}
function testListingPrice(uint256 intendedPrice, string memory priceLabel) internal {
uint32 truncatedPrice = uint32(intendedPrice);
uint256 actualStoredPrice = uint256(truncatedPrice);
console.log("Intended price:", intendedPrice, "(", priceLabel, ")");
console.log("As uint32 (truncated):", truncatedPrice);
console.log("Actual stored value:", actualStoredPrice);
console.log("Actual USDC value:", actualStoredPrice / 10**6, "USDC");
uint256 expectedTruncation = intendedPrice % (MAX_UINT32 + 1);
console.log("Expected truncation:", expectedTruncation);
assertEq(actualStoredPrice, expectedTruncation, "Truncation calculation mismatch");
vm.startPrank(seller);
nftDealers.list(1, uint32(intendedPrice));
(, uint32 storedPrice, , , bool isActive) = nftDealers.s_listings(1);
console.log("\nListing created with:");
console.log(" Stored price (uint32):", storedPrice);
console.log(" Stored price in USDC:", storedPrice / 10**6, "USDC");
console.log(" Seller intended:", intendedPrice / 10**6, "USDC");
assertEq(storedPrice, uint32(intendedPrice), "Price was truncated");
assertLt(storedPrice / 10**6, intendedPrice / 10**6, "Price should be less than intended");
nftDealers.cancelListing(1);
vm.stopPrank();
console.log("---");
}
function testFeeTierUnreachable() public {
console.log("\n=== Fee Tier Unreachability Demonstration ===");
uint256 lowThreshold = 1000e6;
uint256 midThreshold = 10000e6;
uint256 highThreshold = 10001e6;
console.log("Fee Tiers (as designed):");
console.log(" LOW (1%): up to", lowThreshold / 10**6, "USDC");
console.log(" MID (3%):", lowThreshold / 10**6, "-", midThreshold / 10**6, "USDC");
console.log(" HIGH (5%): above", midThreshold / 10**6, "USDC\n");
console.log("Maximum price achievable with uint32:", MAX_UINT32 / 10**6, "USDC\n");
console.log("Attempting to reach MID fee tier (10,000 USDC)...");
uint256 intendedMidPrice = midThreshold;
uint32 truncatedMidPrice = uint32(intendedMidPrice);
console.log(" Intended price:", intendedMidPrice / 10**6, "USDC");
console.log(" Actual stored:", truncatedMidPrice / 10**6, "USDC");
vm.startPrank(seller);
nftDealers.list(1, uint32(intendedMidPrice));
(, uint32 storedPrice, , , ) = nftDealers.s_listings(1);
uint256 fees = nftDealers.calculateFees(storedPrice);
uint256 expectedFeesLow = (storedPrice * 100) / 10000;
console.log(" Actual stored price:", storedPrice / 10**6, "USDC");
console.log(" Fees charged:", fees / 10**6, "USDC");
console.log(" Fee rate:", (fees * 10000) / storedPrice, "bps (should be 100 for 1%)");
assertEq(fees, expectedFeesLow, "Should charge 1% fee, not 3%");
vm.stopPrank();
}
function testPracticalAttack() public {
console.log("\n=== Practical Impact Demonstration ===");
address victim = address(0x5);
deal(address(usdc), victim, 1_000_000 * 10**6);
vm.startPrank(seller);
nftDealers.list(1, uint32(PRICE_50K_USDC));
(, uint32 storedPrice, , , ) = nftDealers.s_listings(1);
vm.stopPrank();
console.log("Seller intends to sell for: 50,000 USDC");
console.log("Actual listing price:", storedPrice / 10**6, "USDC");
console.log("Price difference: 50,000 -", storedPrice / 10**6, "=", 50000 - storedPrice / 10**6, "USDC LOST\n");
vm.startPrank(victim);
usdc.approve(address(nftDealers), storedPrice);
uint256 victimBalanceBefore = usdc.balanceOf(victim);
nftDealers.buy(1);
uint256 victimBalanceAfter = usdc.balanceOf(victim);
console.log("Buyer paid:", (victimBalanceBefore - victimBalanceAfter) / 10**6, "USDC");
console.log("Buyer acquired a NFT they thought was worth 50,000 USDC for only",
(victimBalanceBefore - victimBalanceAfter) / 10**6, "USDC");
vm.stopPrank();
vm.startPrank(seller);
nftDealers.collectUsdcFromSelling(1);
uint256 sellerBalanceAfter = usdc.balanceOf(seller);
console.log("\nSeller received:", (sellerBalanceAfter - 1_000_000 * 10**6) / 10**6, "USDC (including collateral)");
console.log("Seller expected: ~50,000 USDC + 20 USDC collateral");
console.log("Actual loss: ~", 50000 - ((sellerBalanceAfter - 1_000_000 * 10**6) / 10**6 - 20), "USDC");
vm.stopPrank();
}
function testMathematicalExplanation() public pure {
console.log("\n=== Mathematical Explanation ===");
uint32 maxUint32 = type(uint32).max;
uint256 testPrice = 50_000e6;
console.log("uint32 max value:", maxUint32);
console.log("Test price:", testPrice);
console.log("Truncation calculation:");
console.log(" Step 1:", testPrice, "/", uint256(maxUint32) + 1, "=", testPrice / (uint256(maxUint32) + 1), "remainder", testPrice % (uint256(maxUint32) + 1));
console.log(" Step 2:", testPrice, "% 2^32 =", testPrice % (uint256(maxUint32) + 1));
console.log("\nSimple example (using uint8 for clarity):");
console.log(" uint8 max: 255");
console.log(" Trying to store 300 in uint8:");
console.log(" 300 % 256 = 44 (truncated value)");
console.log(" Same principle applies to uint32");
}
}