// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.10; import "forge-std/Test.sol"; import "../src/contracts/PurchaseToken.sol"; import "../src/contracts/TicketNFT.sol"; contract BaseTicketNFTTest is Test { // TicketNFT Events event Transfer( address indexed from, address indexed to, uint256 indexed ticketID ); event Approval( address indexed holder, address indexed approved, uint256 indexed ticketID ); // Primary Market Events event Purchase(address indexed holder, string indexed holderName); // Secondary Market Events event Listing( uint256 indexed ticketID, address indexed holder, uint256 price ); event Purchase( address indexed purchaser, uint256 indexed ticketID, uint256 price, string newName ); event Delisting(uint256 indexed ticketID); uint256 public ticketPriceEth = 1e18; uint256 public ticketPrice = 100e18; PurchaseToken public token; TicketNFT public nft; address public alice = makeAddr("alice"); address public bob = makeAddr("bob"); address public charlie = makeAddr("charlie"); function setUp() public { token = new PurchaseToken(); nft = new TicketNFT(token); } // Add Purchase Tokens to `recipient` to afford `amount` tickets function _topUpTokens(address recipient, uint256 amount) internal { vm.deal(recipient, amount * ticketPriceEth); vm.prank(recipient); token.mint{value: amount * ticketPriceEth}(); } function _buyTicket(address recipient, string memory recipientName) internal { _topUpTokens(recipient, 1); vm.prank(recipient); token.approve(address(nft), ticketPrice); vm.prank(recipient); nft.purchase(recipientName); } } contract TicketNFTTest is BaseTicketNFTTest { function testMint() public { vm.prank(address(nft)); vm.expectEmit(true, true, true, false); emit Transfer(address(0), alice, 1); nft.mint(alice, "alice"); assertEq(nft.balanceOf(alice), 1); assertEq(nft.holderOf(1), alice); assertEq(nft.getApproved(1), address(0)); assertEq(nft.holderNameOf(1), "alice"); assertEq(nft.isExpiredOrUsed(1), false); } function testMintUnauthorized() public { _topUpTokens(alice, 1); vm.prank(alice); vm.expectRevert("mint: can only be called by primary market"); nft.mint(alice, "alice"); assertEq(nft.balanceOf(alice), 0); } function testBalanceOf() public { assertEq(nft.balanceOf(alice), 0); _buyTicket(alice, "alice"); assertEq(nft.balanceOf(alice), 1); } function testHolderOf() public { _buyTicket(alice, "alice"); assertEq(nft.holderOf(1), alice); vm.expectRevert("holderOf: ticket doesn't exist"); nft.holderOf(2); } function testHolderNameOf() public { _buyTicket(alice, "alice"); assertEq(nft.holderNameOf(1), "alice"); vm.expectRevert("holderNameOf: ticket doesn't exist"); nft.holderNameOf(2); } function testUpdateHolderName() public { _buyTicket(alice, "alice1"); assertEq(nft.holderNameOf(1), "alice1"); vm.prank(alice); nft.updateHolderName(1, "alice2"); assertEq(nft.holderNameOf(1), "alice2"); } function testUpdateHolderNameInvalid() public { _buyTicket(alice, "alice1"); assertEq(nft.holderNameOf(1), "alice1"); vm.prank(alice); vm.expectRevert("updateHolderName: ticket doesn't exist"); nft.updateHolderName(2, "alice2"); } function testUpdateHolderNameUnauthorized() public { _buyTicket(alice, "alice"); assertEq(nft.holderNameOf(1), "alice"); vm.prank(bob); vm.expectRevert("updateHolderName: msg.sender doesn't own ticket"); nft.updateHolderName(1, "bob"); assertEq(nft.holderNameOf(1), "alice"); } function testTransfer() public { _buyTicket(alice, "alice"); vm.prank(alice); vm.expectEmit(true, true, true, false); emit Transfer(alice, bob, 1); vm.expectEmit(true, true, true, false); emit Approval(bob, address(0), 1); nft.transferFrom(alice, bob, 1); assertEq(nft.balanceOf(alice), 0); assertEq(nft.balanceOf(bob), 1); assertEq(nft.holderOf(1), bob); assertEq(nft.getApproved(1), address(0)); assertEq(nft.holderNameOf(1), "alice"); assertEq(nft.isExpiredOrUsed(1), false); } function testTransferInvalid() public { _buyTicket(alice, "alice"); vm.prank(alice); vm.expectRevert("transferFrom: from cannot be 0"); nft.transferFrom(address(0), bob, 2); vm.prank(alice); vm.expectRevert("transferFrom: to cannot be 0"); nft.transferFrom(alice, address(0), 2); } function testTransferUnauthorized() public { _buyTicket(alice, "alice"); vm.prank(bob); vm.expectRevert( "transferFrom: msg.sender must be current holder or approved sender" ); nft.transferFrom(alice, bob, 1); } function testApproval() public { _buyTicket(alice, "alice"); vm.prank(alice); vm.expectEmit(true, true, true, false); emit Approval(alice, bob, 1); nft.approve(bob, 1); assertEq(nft.getApproved(1), bob); } function testApprovalInvalid() public { _buyTicket(alice, "alice"); vm.prank(alice); vm.expectRevert("approve: ticket doesn't exist"); nft.approve(bob, 2); } function testApprovalUnauthorized() public { _buyTicket(alice, "alice"); vm.prank(bob); vm.expectRevert("approve: msg.sender doesn't own ticket"); nft.approve(bob, 1); assertEq(nft.getApproved(1), address(0)); } function testGetApprovedInvalid() public { vm.expectRevert("getApproved: ticket doesn't exist"); nft.getApproved(1); } function testTransferFrom() public { _buyTicket(alice, "alice"); vm.prank(alice); nft.approve(bob, 1); vm.prank(bob); vm.expectEmit(true, true, true, false); emit Transfer(alice, charlie, 1); vm.expectEmit(true, true, true, false); emit Approval(charlie, address(0), 1); nft.transferFrom(alice, charlie, 1); assertEq(nft.balanceOf(alice), 0); assertEq(nft.balanceOf(bob), 0); assertEq(nft.balanceOf(charlie), 1); assertEq(nft.holderOf(1), charlie); assertEq(nft.getApproved(1), address(0)); } function testTransferFromInvalid() public { _buyTicket(alice, "alice"); vm.prank(alice); nft.approve(bob, 1); assertEq(nft.getApproved(1), bob); vm.prank(bob); vm.expectRevert("transferFrom: from cannot be 0"); nft.transferFrom(address(0), charlie, 1); vm.prank(bob); vm.expectRevert("transferFrom: to cannot be 0"); nft.transferFrom(alice, address(0), 1); vm.prank(bob); vm.expectRevert("transferFrom: ticket not owned by from"); nft.transferFrom(bob, charlie, 1); vm.prank(bob); vm.expectRevert("transferFrom: ticket not owned by from"); nft.transferFrom(charlie, bob, 1); assertEq(nft.balanceOf(alice), 1); assertEq(nft.balanceOf(bob), 0); assertEq(nft.balanceOf(charlie), 0); assertEq(nft.holderOf(1), alice); assertEq(nft.getApproved(1), bob); } function testTransferFromUnauthorized() public { _buyTicket(alice, "alice"); vm.prank(alice); nft.approve(bob, 1); assertEq(nft.getApproved(1), bob); vm.prank(charlie); vm.expectRevert( "transferFrom: msg.sender must be current holder or approved sender" ); nft.transferFrom(alice, charlie, 1); assertEq(nft.balanceOf(alice), 1); assertEq(nft.balanceOf(bob), 0); assertEq(nft.balanceOf(charlie), 0); assertEq(nft.holderOf(1), alice); assertEq(nft.getApproved(1), bob); } function testIsExpiredOrUsedInvalid() public { vm.expectRevert("isExpiredOrUsed: ticket doesn't exist"); nft.isExpiredOrUsed(1); } function testExpiry() public { _buyTicket(alice, "alice"); assertEq(nft.isExpiredOrUsed(1), false); vm.warp(block.timestamp + 10 days); assertEq(nft.isExpiredOrUsed(1), false); vm.warp(block.timestamp + 1); assertEq(nft.isExpiredOrUsed(1), true); } function testSetUsed() public { _buyTicket(alice, "alice"); assertEq(nft.isExpiredOrUsed(1), false); vm.prank(address(this)); nft.setUsed(1); assertEq(nft.isExpiredOrUsed(1), true); } function testSetUsedNonExistent() public { vm.expectRevert("setUsed: ticket doesn't exist"); nft.setUsed(1); } function testSetUsedAlreadyUsed() public { _buyTicket(alice, "alice"); assertEq(nft.isExpiredOrUsed(1), false); vm.prank(address(this)); nft.setUsed(1); assertEq(nft.isExpiredOrUsed(1), true); vm.prank(address(this)); vm.expectRevert("setUsed: ticket already used"); nft.setUsed(1); } function testSetUsedExpired() public { _buyTicket(alice, "alice"); assertEq(nft.isExpiredOrUsed(1), false); vm.warp(block.timestamp + 10 days + 1); vm.prank(address(this)); vm.expectRevert("setUsed: ticket expired"); nft.setUsed(1); } function testSetUsedUnauthorized() public { _buyTicket(alice, "alice"); assertEq(nft.isExpiredOrUsed(1), false); vm.warp(block.timestamp + 10 days); vm.prank(bob); vm.expectRevert("setUsed: only admin can setUsed"); nft.setUsed(1); } } contract PrimaryMarketTest is BaseTicketNFTTest { function testAdmin() public { assertEq(address(this), nft.admin()); } function testPrimaryPurchase() public { _topUpTokens(alice, 1); vm.prank(alice); token.approve(address(nft), ticketPrice); vm.prank(alice); vm.expectEmit(true, true, true, false); emit Transfer(address(0), alice, 1); vm.expectEmit(true, true, false, false); emit Purchase(alice, "alice"); nft.purchase("alice"); assertEq(nft.balanceOf(alice), 1); assertEq(nft.holderOf(1), alice); assertEq(nft.getApproved(1), address(0)); assertEq(nft.holderNameOf(1), "alice"); assertEq(nft.isExpiredOrUsed(1), false); } function testPrimaryPurchaseInsufficientBalance() public { vm.deal(alice, ticketPriceEth); vm.prank(alice); token.mint{value: ticketPriceEth - 1}(); vm.prank(alice); token.approve(address(nft), ticketPrice); vm.prank(alice); vm.expectRevert("ERC20: transfer amount exceeds balance"); nft.purchase("alice"); assertEq(nft.balanceOf(alice), 0); } function testPrimaryPurchaseInsufficientAllowance() public { vm.deal(alice, ticketPriceEth); vm.prank(alice); token.mint{value: ticketPriceEth}(); vm.prank(alice); token.approve(address(nft), ticketPrice - 1); vm.prank(alice); vm.expectRevert("purchase: insufficient token allowance"); nft.purchase("alice"); assertEq(nft.balanceOf(alice), 0); } function testPrimaryPurchases() public { _topUpTokens(alice, 2); vm.prank(alice); token.approve(address(nft), ticketPrice * 2); vm.prank(alice); nft.purchase("alice"); vm.prank(alice); vm.expectEmit(true, true, true, false); emit Transfer(address(0), alice, 2); vm.expectEmit(true, true, false, false); emit Purchase(alice, "alice's plus one"); nft.purchase("alice's plus one"); assertEq(nft.balanceOf(alice), 2); assertEq(nft.holderOf(2), alice); assertEq(nft.getApproved(2), address(0)); assertEq(nft.holderNameOf(2), "alice's plus one"); assertEq(nft.isExpiredOrUsed(2), false); } function testPurchaseLimit() public { // Set contract totalSupply to 999 using foundry cheats rather than minting repeatedly vm.store(address(nft), bytes32(uint256(1)), bytes32(uint256(999))); // for (uint256 i = 0; i < 999; i++) { // _buyTicket(alice, "alice"); // } _buyTicket(alice, "alice"); // Buy 1000th ticket _topUpTokens(alice, 1); vm.prank(alice); token.approve(address(nft), ticketPrice); vm.prank(alice); vm.expectRevert("purchase: maximum tickets reached"); nft.purchase("alice"); } } contract SecondaryMarketTest is BaseTicketNFTTest { function testList() public { _buyTicket(alice, "alice"); vm.prank(alice); vm.expectEmit(true, true, true, false); emit Transfer(alice, address(nft), 1); vm.expectEmit(true, true, true, false); emit Approval(address(nft), address(0), 1); vm.expectEmit(true, true, false, false); emit Listing(1, alice, 200e18); nft.listTicket(1, 200e18); // Got a flipper here :) assertEq(nft.balanceOf(alice), 0); assertEq(nft.balanceOf(address(nft)), 1); assertEq(nft.holderOf(1), address(nft)); assertEq(nft.getApproved(1), address(0)); assertEq(nft.holderNameOf(1), "alice"); assertEq(nft.isExpiredOrUsed(1), false); } function testListFree() public { _buyTicket(alice, "alice"); vm.prank(alice); vm.expectRevert("listTicket: price cannot be 0"); nft.listTicket(1, 0); // What's the opposite of a flipper? assertEq(nft.balanceOf(alice), 1); assertEq(nft.holderOf(1), alice); } function testListUsed() public { _buyTicket(alice, "alice"); vm.prank(address(this)); nft.setUsed(1); vm.prank(alice); vm.expectRevert("listTicket: ticket is expired/used"); nft.listTicket(1, 200e19); assertEq(nft.balanceOf(alice), 1); assertEq(nft.holderOf(1), alice); assertEq(nft.isExpiredOrUsed(1), true); } function testListExpired() public { _buyTicket(alice, "alice"); vm.warp(block.timestamp + 10 days + 1); vm.prank(alice); vm.expectRevert("listTicket: ticket is expired/used"); nft.listTicket(1, 200e19); assertEq(nft.balanceOf(alice), 1); assertEq(nft.holderOf(1), alice); assertEq(nft.isExpiredOrUsed(1), true); } function testListUnauthorized() public { _buyTicket(alice, "alice"); vm.prank(bob); vm.expectRevert("listTicket: msg.sender doesn't own ticket"); nft.listTicket(1, 200e19); assertEq(nft.balanceOf(alice), 1); assertEq(nft.holderOf(1), alice); } function testDelist() public { _buyTicket(alice, "alice"); vm.prank(alice); nft.listTicket(1, 200e18); vm.prank(alice); vm.expectEmit(true, true, true, false); emit Transfer(address(nft), alice, 1); vm.expectEmit(true, true, true, false); emit Approval(alice, address(0), 1); vm.expectEmit(true, false, false, false); emit Delisting(1); nft.delistTicket(1); assertEq(nft.balanceOf(alice), 1); assertEq(nft.balanceOf(address(nft)), 0); assertEq(nft.holderOf(1), alice); } function testDelistUnauthorized() public { _buyTicket(alice, "alice"); vm.prank(alice); nft.listTicket(1, 200e18); vm.prank(bob); vm.expectRevert("delistTicket: msg.sender didn't list ticket"); nft.delistTicket(1); assertEq(nft.balanceOf(alice), 0); assertEq(nft.balanceOf(bob), 0); assertEq(nft.balanceOf(address(nft)), 1); assertEq(nft.holderOf(1), address(nft)); } function testPurchase() public { uint256 ticketResalePrice = 200e18; uint256 ticketResaleFee = 50 * (ticketResalePrice / 1000); uint256 ticketResaleRevenue = ticketResalePrice - ticketResaleFee; _buyTicket(alice, "alice"); vm.prank(alice); nft.listTicket(1, ticketResalePrice); _topUpTokens(bob, ticketResalePrice / 100e18); vm.prank(bob); token.approve(address(nft), ticketResalePrice); vm.prank(bob); vm.expectEmit(true, true, false, true); emit Purchase(bob, 1, ticketResalePrice, "bob"); nft.purchase(1, "bob"); assertEq(token.balanceOf(alice), ticketResaleRevenue); assertEq(token.balanceOf(bob), 0); assertEq(token.balanceOf(address(this)), ticketPrice + ticketResaleFee); assertEq(nft.balanceOf(alice), 0); assertEq(nft.balanceOf(bob), 1); assertEq(nft.balanceOf(address(nft)), 0); assertEq(nft.holderOf(1), bob); assertEq(nft.getApproved(1), address(0)); assertEq(nft.holderNameOf(1), "bob"); assertEq(nft.isExpiredOrUsed(1), false); } function testPurchaseUsed() public { _buyTicket(alice, "alice"); vm.prank(alice); nft.listTicket(1, 200e18); _topUpTokens(bob, 2); vm.prank(address(this)); nft.setUsed(1); vm.prank(bob); token.approve(address(nft), 200e18); vm.prank(bob); vm.expectRevert("purchase: ticket is expired/used"); nft.purchase(1, "bob"); assertEq(nft.balanceOf(alice), 0); assertEq(nft.balanceOf(bob), 0); assertEq(nft.balanceOf(address(nft)), 1); } function testPurchaseExpired() public { _buyTicket(alice, "alice"); vm.prank(alice); nft.listTicket(1, 200e18); _topUpTokens(bob, 2); vm.warp(block.timestamp + 10 days + 1); vm.prank(bob); token.approve(address(nft), 200e18); vm.prank(bob); vm.expectRevert("purchase: ticket is expired/used"); nft.purchase(1, "bob"); assertEq(nft.balanceOf(alice), 0); assertEq(nft.balanceOf(bob), 0); assertEq(nft.balanceOf(address(nft)), 1); } function testPurchaseInsufficientBalance() public { _buyTicket(alice, "alice"); vm.prank(alice); nft.listTicket(1, 200e18); _topUpTokens(bob, 1); vm.prank(bob); token.approve(address(nft), 200e18); vm.prank(bob); vm.expectRevert("ERC20: transfer amount exceeds balance"); nft.purchase(1, "bob"); assertEq(nft.balanceOf(alice), 0); assertEq(nft.balanceOf(bob), 0); assertEq(nft.balanceOf(address(nft)), 1); } function testPurchaseInsufficientAllowance() public { _buyTicket(alice, "alice"); vm.prank(alice); nft.listTicket(1, 200e18); _topUpTokens(bob, 2); vm.prank(bob); token.approve(address(nft), 100e18); vm.prank(bob); vm.expectRevert("purchase: insufficient token allowance"); nft.purchase(1, "bob"); assertEq(nft.balanceOf(alice), 0); assertEq(nft.balanceOf(bob), 0); assertEq(nft.balanceOf(address(nft)), 1); } function testPurchaseUnlisted() public { _buyTicket(alice, "alice"); vm.prank(alice); nft.listTicket(1, 200e18); vm.prank(alice); nft.delistTicket(1); _topUpTokens(bob, 2); vm.prank(bob); token.approve(address(nft), 200e18); vm.prank(bob); vm.expectRevert( "purchase: ticket is not listed, missing lister address or price" ); nft.purchase(1, "bob"); assertEq(nft.balanceOf(alice), 1); assertEq(nft.balanceOf(bob), 0); assertEq(nft.balanceOf(address(nft)), 0); } }