From 9cfd9409d1b1428cf36ff93f51a5eed9d0680a9e Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 13 Mar 2026 19:43:10 +0100 Subject: [PATCH 01/10] feat: add Mantle mainnet and sepolia deployment setup --- foundry.toml | 2 ++ script/helpers/LibAddressesAndFees.sol | 39 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/foundry.toml b/foundry.toml index ff43113..25e9e8e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -37,6 +37,8 @@ arbitrum-one = "${ARBITRUM_MAINNET_URL}" arbitrum-sepolia = "${ARBITRUM_SEPOLIA_URL}" avalanche = "${AVALANCHE_MAINNET_URL}" avalanche-fuji = "${AVALANCHE_FUJI_URL}" +mantle = "${MANTLE_MAINNET_URL}" +mantle-sepolia = "${MANTLE_SEPOLIA_URL}" [etherscan] mainnet = { key = "${ETHERSCAN_API_KEY}" } diff --git a/script/helpers/LibAddressesAndFees.sol b/script/helpers/LibAddressesAndFees.sol index 27679a9..b246aa8 100644 --- a/script/helpers/LibAddressesAndFees.sol +++ b/script/helpers/LibAddressesAndFees.sol @@ -39,6 +39,9 @@ library LibAddressesAndFees { } else if (_chainId == 4202) { addresses_ = _getLiskSepoliaAddresses(); feeTypes_ = _getLiskSepoliaFeeTypes(); + } else if (_chainId == 5003) { + addresses_ = _getMantleSepoliaAddresses(); + feeTypes_ = _getMantleSepoliaFeeTypes(); } else if (_chainId == 31337) { addresses_ = _getMockAddresses(); feeTypes_ = _getMockFeeTypes(); @@ -221,6 +224,22 @@ library LibAddressesAndFees { addresses_[3] = LISK_SEPOLIA_LSK; } + function _getMantleSepoliaFeeTypes() internal pure returns (uint8[] memory feeType_) { + feeType_ = new uint8[](4); + feeType_[0] = uint8(FeeType.USDC); + feeType_[1] = uint8(FeeType.USDT); + feeType_[2] = uint8(FeeType.LINK); + feeType_[3] = uint8(FeeType.WETH); + } + + function _getMantleSepoliaAddresses() internal pure returns (address[] memory addresses_) { + addresses_ = new address[](4); + addresses_[0] = MANTLE_SEPOLIA_USDC; + addresses_[1] = MANTLE_SEPOLIA_USDT; + addresses_[2] = MANTLE_SEPOLIA_LINK; + addresses_[3] = MANTLE_SEPOLIA_WMNT; + } + function _getMockFeeTypes() internal pure returns (uint8[] memory feeType_) { feeType_ = new uint8[](8); feeType_[0] = uint8(FeeType.WETH); @@ -339,6 +358,26 @@ address constant ARBITRUM_SEPOLIA_USDC = 0x5Df6eD08EEC2fD5e41914d291c0cf48Cd3564 address constant ARBITRUM_SEPOLIA_LINK = 0xb1D4538B4571d411F07960EF2838Ce337FE1E80E; address constant ARBITRUM_SEPOLIA_WETH = 0xE591bf0A0CF924A0674d7792db046B23CEbF5f34; +//*////////////////////////////////////////////////////////////////////////// +// MANTLE ADDRESSES +//////////////////////////////////////////////////////////////////////////*// + +address constant MANTLE_ETH = 0xdEAddEaDdeadDEadDEADDEAddEADDEAddead1111; +address constant MANTLE_USDT = 0x201EBa5CC46D216Ce6DC03F6a759e8E766e956aE; +address constant MANTLE_USDC = 0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9; +address constant MANTLE_LINK = 0xfe36cF0B43aAe49fBc5cFC5c0AF22a623114E043; +address constant MANTLE_WMNT = 0x78c1b0C915c4FAA5FffA6CAbf0219DA63d7f4cb8; + +//*////////////////////////////////////////////////////////////////////////// +// MANTLE SEPOLIA ADDRESSES +//////////////////////////////////////////////////////////////////////////*// + +address constant MANTLE_SEPOLIA_ETH = 0xdEAddEaDdeadDEadDEADDEAddEADDEAddead1111; +address constant MANTLE_SEPOLIA_USDT = 0x201EBa5CC46D216Ce6DC03F6a759e8E766e956aE; +address constant MANTLE_SEPOLIA_USDC = 0x09Bc4E0D864854c6aFB6eB9A9cdF58aC190D0dF9; +address constant MANTLE_SEPOLIA_LINK = 0x22bdEdEa0beBdD7CfFC95bA53826E55afFE9DE04; +address constant MANTLE_SEPOLIA_WMNT = 0x19f5557E23e9914A18239990f6C70D68FDF0deD5; + //*////////////////////////////////////////////////////////////////////////// // TOKENBOUND ADDRESSES //////////////////////////////////////////////////////////////////////////*// From 38fd6305b8cd53e9a10e5750d3f9db7c190e5a69 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 13 Mar 2026 19:45:27 +0100 Subject: [PATCH 02/10] fix: Ignore all broadcast logs --- .gitignore | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 239da05..a66134c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,15 +2,8 @@ cache/ out/ -# Ignores development broadcast logs -!/broadcast -/broadcast/*/31337/ -/broadcast/**/dry-run/ -*broadcast/ - -# Crytic -crytic-export/ -medusa.json +# Ignores all broadcast logs +/broadcast # Docs docs/ From 8393f28c127fff3f20ed51002524ae7a792ff4a0 Mon Sep 17 00:00:00 2001 From: David Dada Date: Sat, 14 Mar 2026 13:54:05 +0100 Subject: [PATCH 03/10] Forge fmt --- src/libs/LibMarketplace.sol | 40 +++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/libs/LibMarketplace.sol b/src/libs/LibMarketplace.sol index 2aaaae5..a69e40b 100644 --- a/src/libs/LibMarketplace.sol +++ b/src/libs/LibMarketplace.sol @@ -48,12 +48,18 @@ library LibMarketplace { ExtraTicketData memory ticketData = _ticketId._getExtraTicketData(); uint48 time = block.timestamp.toUint48(); - if (time < ticketData.purchaseStartTime) revert PurchaseTimeNotReached(); + if (time < ticketData.purchaseStartTime) { + revert PurchaseTimeNotReached(); + } if (time > ticketData.endTime) revert PurchaseTimeNotReached(); - if (ticketData.soldTickets == ticketData.maxTickets) revert TicketSoldOut(); + if (ticketData.soldTickets == ticketData.maxTickets) { + revert TicketSoldOut(); + } ITicket ticket = ITicket(ticketData.ticketAddress); - if (ticket.balanceOf(_buyer) > ticketData.maxTicketsPerUser) revert MaxTicketsHeld(); + if (ticket.balanceOf(_buyer) > ticketData.maxTicketsPerUser) { + revert MaxTicketsHeld(); + } MarketplaceStorage storage ms = _marketplaceStorage(); (uint256 fee, uint256 hostItFee, uint256 totalFee) = _getFees(ms, _ticketId, _feeType); @@ -61,7 +67,9 @@ library LibMarketplace { if (!_isFeeEnabled(ms, _ticketId, _feeType)) revert FeeNotEnabled(); if (_feeType == FeeType.ETH) { - if (msg.value < totalFee) revert InsufficientBalance(address(0), _feeType, totalFee); + if (msg.value < totalFee) { + revert InsufficientBalance(address(0), _feeType, totalFee); + } } else { _payWithToken(ms, _feeType, totalFee); } @@ -86,12 +94,16 @@ library LibMarketplace { _ticketId._checkTicketExists(); uint256 feeTypesLength = _feeTypes.length; - if (feeTypesLength != _fees.length && feeTypesLength > 0) revert InvalidFeeConfig(); + if (feeTypesLength != _fees.length && feeTypesLength > 0) { + revert InvalidFeeConfig(); + } LibFactory._factoryStorage().ticketIdToData[_ticketId].isFree = false; MarketplaceStorage storage ms = _marketplaceStorage(); for (uint256 i; i < feeTypesLength; ++i) { - if (_isFeeEnabled(ms, _ticketId, _feeTypes[i])) revert FeeAlreadySet(); + if (_isFeeEnabled(ms, _ticketId, _feeTypes[i])) { + revert FeeAlreadySet(); + } if (_fees[i] == 0) revert ZeroFee(); ms.feeEnabled[_ticketId][_feeTypes[i]] = true; @@ -110,7 +122,9 @@ library LibMarketplace { uint48 time = block.timestamp.toUint48(); if (time < ticketData.endTime) revert RefundPeriodNotReached(); - if (time > ticketData.endTime + REFUND_PERIOD) revert RefundPeriodExpired(); + if (time > ticketData.endTime + REFUND_PERIOD) { + revert RefundPeriodExpired(); + } address caller = LibContext._msgSender(); ITicket ticket = ITicket(ticketData.ticketAddress); @@ -143,7 +157,9 @@ library LibMarketplace { ExtraTicketData memory ticketData = _ticketId._getExtraTicketData(); if (ticketData.isRefundable) { - if (block.timestamp < ticketData.endTime + REFUND_PERIOD) revert WithdrawPeriodNotReached(); + if (block.timestamp < ticketData.endTime + REFUND_PERIOD) { + revert WithdrawPeriodNotReached(); + } } uint256 balance = _getTicketBalance(_ticketId, _feeType); @@ -187,7 +203,9 @@ library LibMarketplace { address tokenAddress = _getFeeTokenAddress(_ms, _feeType); IERC20 token = IERC20(tokenAddress); - if (token.balanceOf(caller) < _totalFee) revert InsufficientBalance(tokenAddress, _feeType, _totalFee); + if (token.balanceOf(caller) < _totalFee) { + revert InsufficientBalance(tokenAddress, _feeType, _totalFee); + } if (token.allowance(caller, address(this)) < _totalFee) { revert InsufficientAllowance(tokenAddress, _feeType, _totalFee); } @@ -215,7 +233,9 @@ library LibMarketplace { function _setFeeTokenAddresses(FeeType[] calldata _feeTypes, address[] calldata _tokenAddresses) internal { uint256 feeTypesLength = _feeTypes.length; - if (feeTypesLength != _tokenAddresses.length && feeTypesLength > 0) revert InvalidFeeConfig(); + if (feeTypesLength != _tokenAddresses.length && feeTypesLength > 0) { + revert InvalidFeeConfig(); + } for (uint256 i; i < feeTypesLength; ++i) { if (_tokenAddresses[i] == address(0)) revert TokenAddressZero(); _marketplaceStorage().feeTokenAddress[_feeTypes[i]] = _tokenAddresses[i]; From 488cc1f62cc6e748b3845b4fa1f051dd2b367911 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 3 Apr 2026 05:39:06 +0100 Subject: [PATCH 04/10] chore: clean up imports and refactor test helpers Remove unused imports from deploy scripts, extract fee constants, inline facet deployment, add vm labels, and simplify mintTicketETH helper. --- script/DeployHostItTickets.s.sol | 5 --- script/helpers/DeployHostItTicketsHelper.sol | 4 +- test/states/DeployedHostItTickets.sol | 40 +++++++++++--------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/script/DeployHostItTickets.s.sol b/script/DeployHostItTickets.s.sol index b1f420f..74bb9a6 100644 --- a/script/DeployHostItTickets.s.sol +++ b/script/DeployHostItTickets.s.sol @@ -1,15 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.30; -import {DiamondCutFacet} from "@diamond/facets/DiamondCutFacet.sol"; -import {DiamondLoupeFacet} from "@diamond/facets/DiamondLoupeFacet.sol"; -import {OwnableRolesFacet} from "@diamond/facets/OwnableRolesFacet.sol"; -import {DiamondInit} from "@diamond/initializers/DiamondInit.sol"; import {IDiamondCut} from "@diamond/interfaces/IDiamondCut.sol"; import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import {DeployHostItTicketsHelper} from "@ticket-script/helpers/DeployHostItTicketsHelper.sol"; import {LibAddressesAndFees} from "@ticket-script/helpers/LibAddressesAndFees.sol"; -import {HostItTickets} from "@ticket/HostItTickets.sol"; import {CheckInFacet} from "@ticket/facets/CheckInFacet.sol"; import {FactoryFacet} from "@ticket/facets/FactoryFacet.sol"; import {MarketplaceFacet} from "@ticket/facets/MarketplaceFacet.sol"; diff --git a/script/helpers/DeployHostItTicketsHelper.sol b/script/helpers/DeployHostItTicketsHelper.sol index d7e00f8..cde4b73 100644 --- a/script/helpers/DeployHostItTicketsHelper.sol +++ b/script/helpers/DeployHostItTicketsHelper.sol @@ -75,7 +75,9 @@ abstract contract DeployHostItTicketsHelper is GetSelectors, Context { function _getHostItTickets() internal returns (address) { return HOST_IT_TICKETS.code.length == 0 ? address( - new HostItTickets{salt: HOST_IT_SALT}( + new HostItTickets{ + salt: HOST_IT_SALT + }( _createInitFacetCuts(_getDiamondCutFacet(), _getDiamondLoupeFacet(), _getOwnableRolesFacet()), _getDiamondInit(), abi.encodeWithSignature("initDiamond(address)", _msgSender()) diff --git a/test/states/DeployedHostItTickets.sol b/test/states/DeployedHostItTickets.sol index 15a03cc..c363103 100644 --- a/test/states/DeployedHostItTickets.sol +++ b/test/states/DeployedHostItTickets.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.30; -import {FacetCut} from "@diamond-storage/DiamondStorage.sol"; import {IDiamondCut} from "@diamond/interfaces/IDiamondCut.sol"; import {IDiamondLoupe} from "@diamond/interfaces/IDiamondLoupe.sol"; import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; @@ -56,6 +55,10 @@ abstract contract DeployedHostItTickets is Test, DeployHostItTicketsHelper { uint48 public _currentTime = uint48(block.timestamp); + uint256 constant ETH_FEE = 25e14; + uint256 constant USDT_FEE = 10e18; + uint256 constant USDC_FEE = 10e6; + /// @notice Deploys the Diamond contract and initializes interface references and facet addresses. /// @dev This function is intended to be called in a test setup phase (e.g., `setUp()` in Foundry). function setUp() public virtual { @@ -67,11 +70,6 @@ abstract contract DeployedHostItTickets is Test, DeployHostItTicketsHelper { ) ); - // Deploy HostIt facets - address factoryFacetAddr = address(new FactoryFacet()); - address marketplaceFacetAddr = address(new MarketplaceFacet()); - address checkInFacetAddr = address(new CheckInFacet()); - // Deploy initializer address hostItInit = address(new HostItInit()); @@ -91,7 +89,9 @@ abstract contract DeployedHostItTickets is Test, DeployHostItTicketsHelper { // Initialize HostItTickets - called directly from the test (which is the owner) IDiamondCut(hostIt) .diamondCut( - _createHostItFacetCuts(factoryFacetAddr, marketplaceFacetAddr, checkInFacetAddr), + _createHostItFacetCuts( + address(new FactoryFacet()), address(new MarketplaceFacet()), address(new CheckInFacet()) + ), hostItInit, abi.encodeWithSelector(HostItInit.initHostIt.selector, ticketProxy, feeTypes, addresses) ); @@ -105,6 +105,16 @@ abstract contract DeployedHostItTickets is Test, DeployHostItTicketsHelper { facetAddresses = diamondLoupe.facetAddresses(); vm.etch(ERC6551_REGISTRY, address(new ERC6551Registry()).code); + vm.label(alice, "ALICE"); + vm.label(bob, "BOB"); + vm.label(charlie, "CHARLIE"); + vm.label(withdrawer, "WITHDRAWER"); + vm.label(hostIt, "HOSTIT"); + vm.label(ticketProxy, "TICKET_PROXY"); + vm.label(ticketImpl, "TICKET_IMPL"); + vm.label(ticketBeacon, "TICKET_BEACON"); + vm.label(hostItInit, "HOSTIT_INIT"); + vm.label(owner, "TEST_ADDRESS"); } function _mintTicketFree() internal returns (uint64 ticketId_, uint40 tokenId_) { @@ -123,11 +133,7 @@ abstract contract DeployedHostItTickets is Test, DeployHostItTicketsHelper { hoax(alice, totalFee); vm.expectEmit(true, true, true, true, hostIt); emit TicketMinted(ticketId_, FeeType.ETH, totalFee, 1); - (bool success, bytes memory result) = address(marketplaceFacet).call{value: totalFee}( - abi.encodeWithSelector(marketplaceFacet.mintTicket.selector, ticketId_, FeeType.ETH, alice) - ); - assertTrue(success); - tokenId_ = abi.decode(result, (uint40)); + tokenId_ = marketplaceFacet.mintTicket{value: totalFee}(ticketId_, FeeType.ETH, alice); fee_ = fee; hostItFee_ = hostItFee; } @@ -226,7 +232,7 @@ abstract contract DeployedHostItTickets is Test, DeployHostItTicketsHelper { maxTickets: type(uint40).max, maxTicketsPerUser: 0, isFree: false, - isRefundable: true, + isRefundable: false, name: "Paid Ticket", symbol: "", uri: "ipfs://$" @@ -241,7 +247,7 @@ abstract contract DeployedHostItTickets is Test, DeployHostItTicketsHelper { maxTickets: type(uint40).max, maxTicketsPerUser: 0, isFree: false, - isRefundable: true, + isRefundable: false, name: "Updated Paid Ticket", symbol: "UPT", uri: "ipfs://$$" @@ -265,8 +271,8 @@ abstract contract DeployedHostItTickets is Test, DeployHostItTicketsHelper { function _getFees() internal pure returns (uint256[] memory fees_) { fees_ = new uint256[](3); - fees_[0] = 25e14; - fees_[1] = 10e18; - fees_[2] = 10e6; + fees_[0] = ETH_FEE; + fees_[1] = USDT_FEE; + fees_[2] = USDC_FEE; } } From 848b1efbd23cbc24e4610506f687b9fec0c0342c Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 3 Apr 2026 05:39:17 +0100 Subject: [PATCH 05/10] feat: direct payment to organizer for non-refundable tickets Split payment flow: refundable tickets escrow funds in the contract, non-refundable tickets send the ticket fee directly to the organizer. For ERC20 non-refundable payments, the fee is sent to the organizer and the hostIt fee is sent to the contract separately. Also updates _payWithToken to accept a destination address parameter. --- src/libs/LibMarketplace.sol | 51 +++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/libs/LibMarketplace.sol b/src/libs/LibMarketplace.sol index a69e40b..c6da564 100644 --- a/src/libs/LibMarketplace.sol +++ b/src/libs/LibMarketplace.sol @@ -47,13 +47,15 @@ library LibMarketplace { ExtraTicketData memory ticketData = _ticketId._getExtraTicketData(); - uint48 time = block.timestamp.toUint48(); - if (time < ticketData.purchaseStartTime) { - revert PurchaseTimeNotReached(); - } - if (time > ticketData.endTime) revert PurchaseTimeNotReached(); - if (ticketData.soldTickets == ticketData.maxTickets) { - revert TicketSoldOut(); + { + uint48 time = block.timestamp.toUint48(); + if (time < ticketData.purchaseStartTime) { + revert PurchaseTimeNotReached(); + } + if (time > ticketData.endTime) revert PurchaseTimeNotReached(); + if (ticketData.soldTickets == ticketData.maxTickets) { + revert TicketSoldOut(); + } } ITicket ticket = ITicket(ticketData.ticketAddress); @@ -66,15 +68,26 @@ library LibMarketplace { if (!ticketData.isFree) { if (!_isFeeEnabled(ms, _ticketId, _feeType)) revert FeeNotEnabled(); - if (_feeType == FeeType.ETH) { - if (msg.value < totalFee) { - revert InsufficientBalance(address(0), _feeType, totalFee); + if (ticketData.isRefundable) { + if (_feeType == FeeType.ETH) { + if (msg.value < totalFee) { + revert TicketPurchaseFailed(_feeType, totalFee); + } + } else { + _payWithToken(ms, _feeType, totalFee, address(this)); } + ms.ticketBalance[_ticketId][_feeType] += fee; } else { - _payWithToken(ms, _feeType, totalFee); + if (_feeType == FeeType.ETH) { + if (msg.value < totalFee) { + revert TicketPurchaseFailed(_feeType, totalFee); + } + ticketData.ticketAdmin.forceSafeTransferETH(fee); + } else { + _payWithToken(ms, _feeType, fee, ticketData.ticketAdmin); + _payWithToken(ms, _feeType, hostItFee, address(this)); + } } - - ms.ticketBalance[_ticketId][_feeType] += fee; ms.hostItBalance[_feeType] += hostItFee; } @@ -191,14 +204,14 @@ library LibMarketplace { delete _marketplaceStorage().hostItBalance[_feeType]; if (_feeType == FeeType.ETH) { - _to.safeTransferETH(balance); + _to.forceSafeTransferETH(balance); } else { _getFeeTokenAddress(_feeType).safeTransfer(_to, balance); } emit HostItBalanceWithdrawn(_feeType, balance, _to); } - function _payWithToken(MarketplaceStorage storage _ms, FeeType _feeType, uint256 _totalFee) internal { + function _payWithToken(MarketplaceStorage storage _ms, FeeType _feeType, uint256 _totalFee, address _to) internal { address caller = LibContext._msgSender(); address tokenAddress = _getFeeTokenAddress(_ms, _feeType); @@ -209,16 +222,16 @@ library LibMarketplace { if (token.allowance(caller, address(this)) < _totalFee) { revert InsufficientAllowance(tokenAddress, _feeType, _totalFee); } - if (!tokenAddress.trySafeTransferFrom(caller, address(this), _totalFee)) { + if (!tokenAddress.trySafeTransferFrom(caller, _to, _totalFee)) { revert TicketPurchaseFailed(_feeType, _totalFee); } } function _createErc6551Account(address _ticketAddress, uint256 _tokenId) internal { try IERC6551Registry(ERC6551_REGISTRY) - .createAccount(ACCOUNT_V3_IMPLEMENTATION, "", block.chainid, _ticketAddress, _tokenId) returns ( - address account - ) { + .createAccount( + ACCOUNT_V3_IMPLEMENTATION, "", block.chainid, _ticketAddress, _tokenId + ) returns (address account) { if (account == address(0)) { revert CreateERC6551AccountFailed(); } From 4e87e41c51f532f7711f6cca52ed24dbcc43d5c5 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 3 Apr 2026 05:39:27 +0100 Subject: [PATCH 06/10] test: update marketplace tests for direct payment flow Update assertions for non-refundable tickets where fees go directly to the organizer. Comment out refund and ticket balance withdrawal tests (no longer applicable for non-refundable tickets). Add receive() to test contract for ETH transfers. --- test/Marketplace.t.sol | 222 +++++++++++++++++++++-------------------- 1 file changed, 112 insertions(+), 110 deletions(-) diff --git a/test/Marketplace.t.sol b/test/Marketplace.t.sol index 3030a80..800ccb7 100644 --- a/test/Marketplace.t.sol +++ b/test/Marketplace.t.sol @@ -26,7 +26,7 @@ contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { ITicket ticket = ITicket(fullTicketData.ticketAddress); assertEq(ticket.ownerOf(tokenId), alice); assertEq(fullTicketData.soldTickets, 1); - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), fee); + assertEq(alice.balance, 0); assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); } @@ -36,7 +36,7 @@ contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { ITicket ticket = ITicket(fullTicketData.ticketAddress); assertEq(ticket.ownerOf(tokenId), alice); assertEq(fullTicketData.soldTickets, 1); - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), fee); + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), fee); assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), hostItFee); } @@ -46,7 +46,7 @@ contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { ITicket ticket = ITicket(fullTicketData.ticketAddress); assertEq(ticket.ownerOf(tokenId), alice); assertEq(fullTicketData.soldTickets, 1); - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), fee); + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), fee); assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), hostItFee); } @@ -59,118 +59,118 @@ contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { assertEq(marketplaceFacet.getTicketFee(ticketId, FeeType.USDC), _getFees()[2]); } - function test_claimRefundETH() public { - (uint64 ticketId, uint40 tokenId, uint256 ethFee, uint256 hostItFee) = _mintTicketETH(); - FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - ITicket ticket = ITicket(fullTicketData.ticketAddress); - assertEq(ticket.ownerOf(tokenId), alice); - assertEq(fullTicketData.soldTickets, 1); - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), ethFee); - assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); - vm.prank(alice); - ticket.approve(hostIt, tokenId); - vm.prank(alice); - vm.warp(fullTicketData.endTime); - vm.expectEmit(true, true, true, true, hostIt); - emit TicketRefunded(ticketId, FeeType.ETH, ethFee, bob); - marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, bob); - assertEq(ticket.ownerOf(tokenId), owner); - assertEq(fullTicketData.soldTickets, 1); - assertEq(bob.balance, ethFee); - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); - assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); - } + // function test_claimRefundETH() public { + // (uint64 ticketId, uint40 tokenId, uint256 ethFee, uint256 hostItFee) = _mintTicketETH(); + // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + // ITicket ticket = ITicket(fullTicketData.ticketAddress); + // assertEq(ticket.ownerOf(tokenId), alice); + // assertEq(fullTicketData.soldTickets, 1); + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), ethFee); + // assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); + // vm.prank(alice); + // ticket.approve(hostIt, tokenId); + // vm.prank(alice); + // vm.warp(fullTicketData.endTime); + // vm.expectEmit(true, true, true, true, hostIt); + // emit TicketRefunded(ticketId, FeeType.ETH, ethFee, bob); + // marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, bob); + // assertEq(ticket.ownerOf(tokenId), owner); + // assertEq(fullTicketData.soldTickets, 1); + // assertEq(bob.balance, ethFee); + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); + // assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); + // } - function test_claimRefundUSDT() public { - (uint64 ticketId, uint40 tokenId, uint256 usdtFee, uint256 hostItFee, ERC20Mock usdt) = _mintTicketUSDT(); - FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - ITicket ticket = ITicket(fullTicketData.ticketAddress); - assertEq(ticket.ownerOf(tokenId), alice); - assertEq(fullTicketData.soldTickets, 1); - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), usdtFee); - assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), hostItFee); - vm.prank(alice); - ticket.approve(hostIt, tokenId); - vm.prank(alice); - vm.warp(fullTicketData.endTime); - vm.expectEmit(true, true, true, true, hostIt); - emit TicketRefunded(ticketId, FeeType.USDT, usdtFee, bob); - marketplaceFacet.claimRefund(ticketId, FeeType.USDT, tokenId, bob); - assertEq(ticket.ownerOf(tokenId), owner); - assertEq(fullTicketData.soldTickets, 1); - assertEq(usdt.balanceOf(bob), usdtFee); - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), 0); - assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), hostItFee); - } + // function test_claimRefundUSDT() public { + // (uint64 ticketId, uint40 tokenId, uint256 usdtFee, uint256 hostItFee, ERC20Mock usdt) = _mintTicketUSDT(); + // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + // ITicket ticket = ITicket(fullTicketData.ticketAddress); + // assertEq(ticket.ownerOf(tokenId), alice); + // assertEq(fullTicketData.soldTickets, 1); + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), usdtFee); + // assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), hostItFee); + // vm.prank(alice); + // ticket.approve(hostIt, tokenId); + // vm.prank(alice); + // vm.warp(fullTicketData.endTime); + // vm.expectEmit(true, true, true, true, hostIt); + // emit TicketRefunded(ticketId, FeeType.USDT, usdtFee, bob); + // marketplaceFacet.claimRefund(ticketId, FeeType.USDT, tokenId, bob); + // assertEq(ticket.ownerOf(tokenId), owner); + // assertEq(fullTicketData.soldTickets, 1); + // assertEq(usdt.balanceOf(bob), usdtFee); + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), 0); + // assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), hostItFee); + // } - function test_claimRefundUSDC() public { - (uint64 ticketId, uint40 tokenId, uint256 usdcFee, uint256 hostItFee, ERC20Mock usdc) = _mintTicketUSDC(); - FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - ITicket ticket = ITicket(fullTicketData.ticketAddress); - assertEq(ticket.ownerOf(tokenId), alice); - assertEq(fullTicketData.soldTickets, 1); - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), usdcFee); - assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), hostItFee); - vm.prank(alice); - ticket.approve(hostIt, tokenId); - vm.prank(alice); - vm.warp(fullTicketData.endTime); - vm.expectEmit(true, true, true, true, hostIt); - emit TicketRefunded(ticketId, FeeType.USDC, usdcFee, bob); - marketplaceFacet.claimRefund(ticketId, FeeType.USDC, tokenId, bob); - assertEq(ticket.ownerOf(tokenId), owner); - assertEq(fullTicketData.soldTickets, 1); - assertEq(usdc.balanceOf(bob), usdcFee); - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), 0); - assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), hostItFee); - } + // function test_claimRefundUSDC() public { + // (uint64 ticketId, uint40 tokenId, uint256 usdcFee, uint256 hostItFee, ERC20Mock usdc) = _mintTicketUSDC(); + // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + // ITicket ticket = ITicket(fullTicketData.ticketAddress); + // assertEq(ticket.ownerOf(tokenId), alice); + // assertEq(fullTicketData.soldTickets, 1); + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), usdcFee); + // assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), hostItFee); + // vm.prank(alice); + // ticket.approve(hostIt, tokenId); + // vm.prank(alice); + // vm.warp(fullTicketData.endTime); + // vm.expectEmit(true, true, true, true, hostIt); + // emit TicketRefunded(ticketId, FeeType.USDC, usdcFee, bob); + // marketplaceFacet.claimRefund(ticketId, FeeType.USDC, tokenId, bob); + // assertEq(ticket.ownerOf(tokenId), owner); + // assertEq(fullTicketData.soldTickets, 1); + // assertEq(usdc.balanceOf(bob), usdcFee); + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), 0); + // assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), hostItFee); + // } - function test_withdrawTicketBalanceETH() public { - (uint64 ticketId,, uint256 ethFee,) = _mintTicketETH(); - // Check platform balances before withdraw - // Withdraw - FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); - vm.expectEmit(true, true, true, true, hostIt); - emit TicketBalanceWithdrawn(ticketId, FeeType.ETH, ethFee, withdrawer); - marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.ETH, withdrawer); - // Check platform balances after withdraw - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); - // Check vault balances after withdraw - assertEq(withdrawer.balance, ethFee); - } + // function test_withdrawTicketBalanceETH() public { + // (uint64 ticketId,, uint256 ethFee,) = _mintTicketETH(); + // // Check platform balances before withdraw + // // Withdraw + // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + // vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); + // vm.expectEmit(true, true, true, true, hostIt); + // emit TicketBalanceWithdrawn(ticketId, FeeType.ETH, ethFee, withdrawer); + // marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.ETH, withdrawer); + // // Check platform balances after withdraw + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); + // // Check vault balances after withdraw + // assertEq(withdrawer.balance, ethFee); + // } - function test_withdrawTicketBalanceUSDT() public { - (uint64 ticketId,, uint256 usdtFee,, ERC20Mock usdt) = _mintTicketUSDT(); - // Check platform balances before withdraw - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), usdtFee); - // Withdraw - FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); - vm.expectEmit(true, true, true, true, hostIt); - emit TicketBalanceWithdrawn(ticketId, FeeType.USDT, usdtFee, withdrawer); - marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.USDT, withdrawer); - // Check platform balances after withdraw - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), 0); - // Check owner balances after withdraw - assertEq(usdt.balanceOf(withdrawer), usdtFee); - } + // function test_withdrawTicketBalanceUSDT() public { + // (uint64 ticketId,, uint256 usdtFee,, ERC20Mock usdt) = _mintTicketUSDT(); + // // Check platform balances before withdraw + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), usdtFee); + // // Withdraw + // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + // vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); + // vm.expectEmit(true, true, true, true, hostIt); + // emit TicketBalanceWithdrawn(ticketId, FeeType.USDT, usdtFee, withdrawer); + // marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.USDT, withdrawer); + // // Check platform balances after withdraw + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), 0); + // // Check owner balances after withdraw + // assertEq(usdt.balanceOf(withdrawer), usdtFee); + // } - function test_withdrawTicketBalanceUSDC() public { - (uint64 ticketId,, uint256 usdcFee,, ERC20Mock usdc) = _mintTicketUSDC(); - // Check platform balances before withdraw - FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), usdcFee); - // Withdraw - vm.expectEmit(true, true, true, true, hostIt); - emit TicketBalanceWithdrawn(ticketId, FeeType.USDC, usdcFee, withdrawer); - marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.USDC, withdrawer); - // Check platform balances after withdraw - assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), 0); - // Check owner balances after withdraw - assertEq(usdc.balanceOf(withdrawer), usdcFee); - } + // function test_withdrawTicketBalanceUSDC() public { + // (uint64 ticketId,, uint256 usdcFee,, ERC20Mock usdc) = _mintTicketUSDC(); + // // Check platform balances before withdraw + // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + // vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), usdcFee); + // // Withdraw + // vm.expectEmit(true, true, true, true, hostIt); + // emit TicketBalanceWithdrawn(ticketId, FeeType.USDC, usdcFee, withdrawer); + // marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.USDC, withdrawer); + // // Check platform balances after withdraw + // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), 0); + // // Check owner balances after withdraw + // assertEq(usdc.balanceOf(withdrawer), usdcFee); + // } function test_withdrawHostItBalanceETH() public { (,,, uint256 hostItFee) = _mintTicketETH(); @@ -198,4 +198,6 @@ contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), 0); assertEq(usdc.balanceOf(withdrawer), hostItFee); } + + receive() external payable {} } From 801561b1b7c6a469741b7a0a8ceacb7bd2a262b4 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 3 Apr 2026 05:50:52 +0100 Subject: [PATCH 07/10] test: add extensive marketplace tests for direct payment and refundable flows Expand from 8 to 58 marketplace tests covering: - Non-refundable direct payment: organizer receives fee, contract holds only hostIt fee, no escrow (ETH, USDT, USDC) - Refundable escrow: funds escrowed, refund claims, withdraw after refund period, time-window revert cases (ETH, USDT, USDC) - Revert cases: insufficient balance/allowance, refund on non-refundable, withdraw zero balance, disabled fee type - Mixed scenarios: hostIt fee accumulation across ticket types - Add refundable ticket helpers to DeployedHostItTickets base --- test/Marketplace.t.sol | 644 ++++++++++++++++++++------ test/states/DeployedHostItTickets.sol | 75 +++ 2 files changed, 585 insertions(+), 134 deletions(-) diff --git a/test/Marketplace.t.sol b/test/Marketplace.t.sol index 800ccb7..72c131c 100644 --- a/test/Marketplace.t.sol +++ b/test/Marketplace.t.sol @@ -9,8 +9,14 @@ import {DeployedHostItTickets} from "@ticket-test/states/DeployedHostItTickets.s import {ITicket} from "@ticket/interfaces/ITicket.sol"; /// forge-lint: disable-next-line(unaliased-plain-import) import "@ticket-logs/MarketplaceLogs.sol"; +/// forge-lint: disable-next-line(unaliased-plain-import) +import "@ticket-errors/MarketplaceErrors.sol"; contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { + // ====================================================================== + // FREE TICKET MINTING + // ====================================================================== + function test_mintFreeTicket() public { vm.prank(alice); (uint64 ticketId, uint40 tokenId) = _mintTicketFree(); @@ -20,159 +26,234 @@ contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { assertEq(fullTicketData.soldTickets, 1); } - function test_mintPaidTicketETH() public { - (uint64 ticketId, uint40 tokenId, uint256 fee, uint256 hostItFee) = _mintTicketETH(); - FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - ITicket ticket = ITicket(fullTicketData.ticketAddress); - assertEq(ticket.ownerOf(tokenId), alice); - assertEq(fullTicketData.soldTickets, 1); + function test_mintFreeTicket_noBalanceChanges() public { + vm.prank(alice); + (uint64 ticketId,) = _mintTicketFree(); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), 0); + } + + // ====================================================================== + // NON-REFUNDABLE DIRECT PAYMENT: ETH + // ====================================================================== + + function test_directPayment_ETH_organizerReceivesFee() public { + uint256 ownerBalanceBefore = owner.balance; + (,, uint256 fee,) = _mintTicketETH(); + assertEq(owner.balance - ownerBalanceBefore, fee); + } + + function test_directPayment_ETH_buyerSpendsFullAmount() public { + _mintTicketETH(); assertEq(alice.balance, 0); + } + + function test_directPayment_ETH_hostItFeeAccumulated() public { + (,,, uint256 hostItFee) = _mintTicketETH(); assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); } - function test_mintPaidTicketUSDT() public { - (uint64 ticketId, uint40 tokenId, uint256 fee, uint256 hostItFee,) = _mintTicketUSDT(); + function test_directPayment_ETH_noTicketBalanceEscrowed() public { + (uint64 ticketId,,,) = _mintTicketETH(); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); + } + + function test_directPayment_ETH_ticketMintedToBuyer() public { + (uint64 ticketId, uint40 tokenId,,) = _mintTicketETH(); FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); ITicket ticket = ITicket(fullTicketData.ticketAddress); assertEq(ticket.ownerOf(tokenId), alice); assertEq(fullTicketData.soldTickets, 1); - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), fee); + } + + function test_directPayment_ETH_contractHoldsOnlyHostItFee() public { + uint256 contractBalanceBefore = hostIt.balance; + (,,, uint256 hostItFee) = _mintTicketETH(); + assertEq(hostIt.balance - contractBalanceBefore, hostItFee); + } + + function test_directPayment_ETH_emitsTicketMinted() public { + _createPaidTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + (,, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + hoax(alice, totalFee); + vm.expectEmit(true, true, true, true, hostIt); + emit TicketMinted(ticketId, FeeType.ETH, totalFee, 1); + marketplaceFacet.mintTicket{value: totalFee}(ticketId, FeeType.ETH, alice); + } + + function test_directPayment_ETH_multipleBuyersAccumulateHostItFees() public { + _createPaidTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + (, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + + hoax(alice, totalFee); + marketplaceFacet.mintTicket{value: totalFee}(ticketId, FeeType.ETH, alice); + + hoax(bob, totalFee); + marketplaceFacet.mintTicket{value: totalFee}(ticketId, FeeType.ETH, bob); + + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee * 2); + } + + function test_directPayment_ETH_multipleBuyersPayOrganizer() public { + _createPaidTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + (uint256 fee,, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + + uint256 ownerBalanceBefore = owner.balance; + + hoax(alice, totalFee); + marketplaceFacet.mintTicket{value: totalFee}(ticketId, FeeType.ETH, alice); + + hoax(bob, totalFee); + marketplaceFacet.mintTicket{value: totalFee}(ticketId, FeeType.ETH, bob); + + assertEq(owner.balance - ownerBalanceBefore, fee * 2); + } + + function test_directPayment_ETH_revertsInsufficientValue() public { + _createPaidTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + (,, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + hoax(alice, totalFee); + vm.expectRevert(abi.encodeWithSelector(TicketPurchaseFailed.selector, FeeType.ETH, totalFee)); + marketplaceFacet.mintTicket{value: totalFee - 1}(ticketId, FeeType.ETH, alice); + } + + // ====================================================================== + // NON-REFUNDABLE DIRECT PAYMENT: USDT + // ====================================================================== + + function test_directPayment_USDT_organizerReceivesFee() public { + (,, uint256 fee,,) = _mintTicketUSDT(); + ERC20Mock usdt = ERC20Mock(marketplaceFacet.getFeeTokenAddress(FeeType.USDT)); + assertEq(usdt.balanceOf(owner), fee); + } + + function test_directPayment_USDT_hostItFeeAccumulated() public { + (,,, uint256 hostItFee,) = _mintTicketUSDT(); assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), hostItFee); } - function test_mintPaidTicketUSDC() public { - (uint64 ticketId, uint40 tokenId, uint256 fee, uint256 hostItFee,) = _mintTicketUSDC(); + function test_directPayment_USDT_noTicketBalanceEscrowed() public { + (uint64 ticketId,,,,) = _mintTicketUSDT(); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), 0); + } + + function test_directPayment_USDT_contractHoldsHostItFee() public { + (,,, uint256 hostItFee, ERC20Mock usdt) = _mintTicketUSDT(); + assertEq(usdt.balanceOf(hostIt), hostItFee); + } + + function test_directPayment_USDT_buyerBalanceZero() public { + (,,,, ERC20Mock usdt) = _mintTicketUSDT(); + assertEq(usdt.balanceOf(alice), 0); + } + + function test_directPayment_USDT_ticketMintedToBuyer() public { + (uint64 ticketId, uint40 tokenId,,,) = _mintTicketUSDT(); FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); ITicket ticket = ITicket(fullTicketData.ticketAddress); assertEq(ticket.ownerOf(tokenId), alice); assertEq(fullTicketData.soldTickets, 1); - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), fee); + } + + // ====================================================================== + // NON-REFUNDABLE DIRECT PAYMENT: USDC + // ====================================================================== + + function test_directPayment_USDC_organizerReceivesFee() public { + (,, uint256 fee,,) = _mintTicketUSDC(); + ERC20Mock usdc = ERC20Mock(marketplaceFacet.getFeeTokenAddress(FeeType.USDC)); + assertEq(usdc.balanceOf(owner), fee); + } + + function test_directPayment_USDC_hostItFeeAccumulated() public { + (,,, uint256 hostItFee,) = _mintTicketUSDC(); assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), hostItFee); } - function test_setTicketFees() public { - _createFreeTicket(); + function test_directPayment_USDC_noTicketBalanceEscrowed() public { + (uint64 ticketId,,,,) = _mintTicketUSDC(); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), 0); + } + + function test_directPayment_USDC_contractHoldsHostItFee() public { + (,,, uint256 hostItFee, ERC20Mock usdc) = _mintTicketUSDC(); + assertEq(usdc.balanceOf(hostIt), hostItFee); + } + + function test_directPayment_USDC_ticketMintedToBuyer() public { + (uint64 ticketId, uint40 tokenId,,,) = _mintTicketUSDC(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(fullTicketData.ticketAddress); + assertEq(ticket.ownerOf(tokenId), alice); + assertEq(fullTicketData.soldTickets, 1); + } + + // ====================================================================== + // NON-REFUNDABLE: TOKEN REVERT CASES + // ====================================================================== + + function test_directPayment_USDT_revertsInsufficientBalance() public { + _createPaidTicket(); uint64 ticketId = factoryFacet.ticketCount(); - marketplaceFacet.setTicketFees(ticketId, _getFeeTypes(), _getFees()); - assertEq(marketplaceFacet.getTicketFee(ticketId, FeeType.ETH), _getFees()[0]); - assertEq(marketplaceFacet.getTicketFee(ticketId, FeeType.USDT), _getFees()[1]); - assertEq(marketplaceFacet.getTicketFee(ticketId, FeeType.USDC), _getFees()[2]); + (, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.USDT); + ERC20Mock usdt = ERC20Mock(marketplaceFacet.getFeeTokenAddress(FeeType.USDT)); + // Mint just under totalFee so balance check fails on the second _payWithToken call (hostItFee) + usdt.mint(alice, totalFee - 1); + vm.prank(alice); + usdt.approve(address(marketplaceFacet), totalFee); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector, address(usdt), FeeType.USDT, hostItFee)); + marketplaceFacet.mintTicket(ticketId, FeeType.USDT, alice); + } + + function test_directPayment_USDT_revertsInsufficientAllowance() public { + _createPaidTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + (, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.USDT); + ERC20Mock usdt = ERC20Mock(marketplaceFacet.getFeeTokenAddress(FeeType.USDT)); + usdt.mint(alice, totalFee); + vm.prank(alice); + // Approve just under totalFee so allowance check fails on the second _payWithToken call (hostItFee) + usdt.approve(address(marketplaceFacet), totalFee - 1); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(InsufficientAllowance.selector, address(usdt), FeeType.USDT, hostItFee)); + marketplaceFacet.mintTicket(ticketId, FeeType.USDT, alice); + } + + // ====================================================================== + // NON-REFUNDABLE: REFUND NOT ALLOWED + // ====================================================================== + + function test_directPayment_claimRefundRevertsForNonRefundable() public { + (uint64 ticketId, uint40 tokenId,,) = _mintTicketETH(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + vm.warp(fullTicketData.endTime); + vm.prank(alice); + vm.expectRevert(RefundNotEnabled.selector); + marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, alice); + } + + // ====================================================================== + // NON-REFUNDABLE: WITHDRAW TICKET BALANCE REVERTS + // ====================================================================== + + function test_directPayment_withdrawTicketBalanceRevertsNoBalance() public { + (uint64 ticketId,,,) = _mintTicketETH(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); + vm.expectRevert(InsufficientWithdrawBalance.selector); + marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.ETH, withdrawer); } - // function test_claimRefundETH() public { - // (uint64 ticketId, uint40 tokenId, uint256 ethFee, uint256 hostItFee) = _mintTicketETH(); - // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - // ITicket ticket = ITicket(fullTicketData.ticketAddress); - // assertEq(ticket.ownerOf(tokenId), alice); - // assertEq(fullTicketData.soldTickets, 1); - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), ethFee); - // assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); - // vm.prank(alice); - // ticket.approve(hostIt, tokenId); - // vm.prank(alice); - // vm.warp(fullTicketData.endTime); - // vm.expectEmit(true, true, true, true, hostIt); - // emit TicketRefunded(ticketId, FeeType.ETH, ethFee, bob); - // marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, bob); - // assertEq(ticket.ownerOf(tokenId), owner); - // assertEq(fullTicketData.soldTickets, 1); - // assertEq(bob.balance, ethFee); - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); - // assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); - // } - - // function test_claimRefundUSDT() public { - // (uint64 ticketId, uint40 tokenId, uint256 usdtFee, uint256 hostItFee, ERC20Mock usdt) = _mintTicketUSDT(); - // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - // ITicket ticket = ITicket(fullTicketData.ticketAddress); - // assertEq(ticket.ownerOf(tokenId), alice); - // assertEq(fullTicketData.soldTickets, 1); - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), usdtFee); - // assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), hostItFee); - // vm.prank(alice); - // ticket.approve(hostIt, tokenId); - // vm.prank(alice); - // vm.warp(fullTicketData.endTime); - // vm.expectEmit(true, true, true, true, hostIt); - // emit TicketRefunded(ticketId, FeeType.USDT, usdtFee, bob); - // marketplaceFacet.claimRefund(ticketId, FeeType.USDT, tokenId, bob); - // assertEq(ticket.ownerOf(tokenId), owner); - // assertEq(fullTicketData.soldTickets, 1); - // assertEq(usdt.balanceOf(bob), usdtFee); - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), 0); - // assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), hostItFee); - // } - - // function test_claimRefundUSDC() public { - // (uint64 ticketId, uint40 tokenId, uint256 usdcFee, uint256 hostItFee, ERC20Mock usdc) = _mintTicketUSDC(); - // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - // ITicket ticket = ITicket(fullTicketData.ticketAddress); - // assertEq(ticket.ownerOf(tokenId), alice); - // assertEq(fullTicketData.soldTickets, 1); - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), usdcFee); - // assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), hostItFee); - // vm.prank(alice); - // ticket.approve(hostIt, tokenId); - // vm.prank(alice); - // vm.warp(fullTicketData.endTime); - // vm.expectEmit(true, true, true, true, hostIt); - // emit TicketRefunded(ticketId, FeeType.USDC, usdcFee, bob); - // marketplaceFacet.claimRefund(ticketId, FeeType.USDC, tokenId, bob); - // assertEq(ticket.ownerOf(tokenId), owner); - // assertEq(fullTicketData.soldTickets, 1); - // assertEq(usdc.balanceOf(bob), usdcFee); - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), 0); - // assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), hostItFee); - // } - - // function test_withdrawTicketBalanceETH() public { - // (uint64 ticketId,, uint256 ethFee,) = _mintTicketETH(); - // // Check platform balances before withdraw - // // Withdraw - // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - // vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); - // vm.expectEmit(true, true, true, true, hostIt); - // emit TicketBalanceWithdrawn(ticketId, FeeType.ETH, ethFee, withdrawer); - // marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.ETH, withdrawer); - // // Check platform balances after withdraw - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); - // // Check vault balances after withdraw - // assertEq(withdrawer.balance, ethFee); - // } - - // function test_withdrawTicketBalanceUSDT() public { - // (uint64 ticketId,, uint256 usdtFee,, ERC20Mock usdt) = _mintTicketUSDT(); - // // Check platform balances before withdraw - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), usdtFee); - // // Withdraw - // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - // vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); - // vm.expectEmit(true, true, true, true, hostIt); - // emit TicketBalanceWithdrawn(ticketId, FeeType.USDT, usdtFee, withdrawer); - // marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.USDT, withdrawer); - // // Check platform balances after withdraw - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), 0); - // // Check owner balances after withdraw - // assertEq(usdt.balanceOf(withdrawer), usdtFee); - // } - - // function test_withdrawTicketBalanceUSDC() public { - // (uint64 ticketId,, uint256 usdcFee,, ERC20Mock usdc) = _mintTicketUSDC(); - // // Check platform balances before withdraw - // FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); - // vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), usdcFee); - // // Withdraw - // vm.expectEmit(true, true, true, true, hostIt); - // emit TicketBalanceWithdrawn(ticketId, FeeType.USDC, usdcFee, withdrawer); - // marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.USDC, withdrawer); - // // Check platform balances after withdraw - // assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), 0); - // // Check owner balances after withdraw - // assertEq(usdc.balanceOf(withdrawer), usdcFee); - // } - - function test_withdrawHostItBalanceETH() public { + // ====================================================================== + // NON-REFUNDABLE: WITHDRAW HOSTIT BALANCE + // ====================================================================== + + function test_directPayment_withdrawHostItBalanceETH() public { (,,, uint256 hostItFee) = _mintTicketETH(); vm.expectEmit(true, true, true, true, hostIt); emit HostItBalanceWithdrawn(FeeType.ETH, hostItFee, withdrawer); @@ -181,7 +262,7 @@ contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { assertEq(withdrawer.balance, hostItFee); } - function test_withdrawHostItBalanceUSDT() public { + function test_directPayment_withdrawHostItBalanceUSDT() public { (,,, uint256 hostItFee, ERC20Mock usdt) = _mintTicketUSDT(); vm.expectEmit(true, true, true, true, hostIt); emit HostItBalanceWithdrawn(FeeType.USDT, hostItFee, withdrawer); @@ -190,7 +271,7 @@ contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { assertEq(usdt.balanceOf(withdrawer), hostItFee); } - function test_withdrawHostItBalanceUSDC() public { + function test_directPayment_withdrawHostItBalanceUSDC() public { (,,, uint256 hostItFee, ERC20Mock usdc) = _mintTicketUSDC(); vm.expectEmit(true, true, true, true, hostIt); emit HostItBalanceWithdrawn(FeeType.USDC, hostItFee, withdrawer); @@ -199,5 +280,300 @@ contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { assertEq(usdc.balanceOf(withdrawer), hostItFee); } + function test_directPayment_withdrawHostItBalanceRevertsIfZero() public { + vm.expectRevert(InsufficientWithdrawBalance.selector); + marketplaceFacet.withdrawHostItBalance(FeeType.ETH, withdrawer); + } + + // ====================================================================== + // REFUNDABLE ESCROW: ETH + // ====================================================================== + + function test_refundable_ETH_fundsEscrowed() public { + (uint64 ticketId,, uint256 fee, uint256 hostItFee) = _mintTicketETHRefundable(); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), fee); + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); + } + + function test_refundable_ETH_organizerDoesNotReceiveFee() public { + uint256 ownerBalanceBefore = owner.balance; + _mintTicketETHRefundable(); + assertEq(owner.balance, ownerBalanceBefore); + } + + function test_refundable_ETH_contractHoldsTotalFee() public { + uint256 contractBalanceBefore = hostIt.balance; + (,, uint256 fee, uint256 hostItFee) = _mintTicketETHRefundable(); + assertEq(hostIt.balance - contractBalanceBefore, fee + hostItFee); + } + + function test_refundable_ETH_ticketMintedToBuyer() public { + (uint64 ticketId, uint40 tokenId,,) = _mintTicketETHRefundable(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(fullTicketData.ticketAddress); + assertEq(ticket.ownerOf(tokenId), alice); + assertEq(fullTicketData.soldTickets, 1); + } + + function test_refundable_ETH_claimRefund() public { + (uint64 ticketId, uint40 tokenId, uint256 fee, uint256 hostItFee) = _mintTicketETHRefundable(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(fullTicketData.ticketAddress); + + vm.prank(alice); + ticket.approve(hostIt, tokenId); + + vm.warp(fullTicketData.endTime); + vm.prank(alice); + vm.expectEmit(true, true, true, true, hostIt); + emit TicketRefunded(ticketId, FeeType.ETH, fee, bob); + marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, bob); + + assertEq(ticket.ownerOf(tokenId), owner); + assertEq(bob.balance, fee); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); + } + + function test_refundable_ETH_claimRefundRevertsBeforeEndTime() public { + (uint64 ticketId, uint40 tokenId,,) = _mintTicketETHRefundable(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(fullTicketData.ticketAddress); + + vm.prank(alice); + ticket.approve(hostIt, tokenId); + + vm.warp(fullTicketData.endTime - 1); + vm.prank(alice); + vm.expectRevert(RefundPeriodNotReached.selector); + marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, bob); + } + + function test_refundable_ETH_claimRefundRevertsAfterRefundPeriod() public { + (uint64 ticketId, uint40 tokenId,,) = _mintTicketETHRefundable(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(fullTicketData.ticketAddress); + + vm.prank(alice); + ticket.approve(hostIt, tokenId); + + vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod() + 1); + vm.prank(alice); + vm.expectRevert(RefundPeriodExpired.selector); + marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, bob); + } + + function test_refundable_ETH_claimRefundRevertsNonOwner() public { + (uint64 ticketId, uint40 tokenId,,) = _mintTicketETHRefundable(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + + vm.warp(fullTicketData.endTime); + vm.prank(bob); + vm.expectRevert(abi.encodeWithSelector(TicketNotOwned.selector, tokenId)); + marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, bob); + } + + function test_refundable_ETH_withdrawTicketBalanceAfterRefundPeriod() public { + (uint64 ticketId,, uint256 fee,) = _mintTicketETHRefundable(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + + vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); + vm.expectEmit(true, true, true, true, hostIt); + emit TicketBalanceWithdrawn(ticketId, FeeType.ETH, fee, withdrawer); + marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.ETH, withdrawer); + + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); + assertEq(withdrawer.balance, fee); + } + + function test_refundable_ETH_withdrawTicketBalanceRevertsBeforeRefundPeriod() public { + (uint64 ticketId,,,) = _mintTicketETHRefundable(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + + vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod() - 1); + vm.expectRevert(WithdrawPeriodNotReached.selector); + marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.ETH, withdrawer); + } + + // ====================================================================== + // REFUNDABLE ESCROW: USDT + // ====================================================================== + + function test_refundable_USDT_fundsEscrowed() public { + (uint64 ticketId,, uint256 fee, uint256 hostItFee,) = _mintTicketUSDTRefundable(); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), fee); + assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), hostItFee); + } + + function test_refundable_USDT_organizerDoesNotReceiveFee() public { + (,,,, ERC20Mock usdt) = _mintTicketUSDTRefundable(); + assertEq(usdt.balanceOf(owner), 0); + } + + function test_refundable_USDT_contractHoldsTotalFee() public { + (,, uint256 fee, uint256 hostItFee, ERC20Mock usdt) = _mintTicketUSDTRefundable(); + assertEq(usdt.balanceOf(hostIt), fee + hostItFee); + } + + function test_refundable_USDT_claimRefund() public { + (uint64 ticketId, uint40 tokenId, uint256 fee, uint256 hostItFee, ERC20Mock usdt) = _mintTicketUSDTRefundable(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(fullTicketData.ticketAddress); + + vm.prank(alice); + ticket.approve(hostIt, tokenId); + + vm.warp(fullTicketData.endTime); + vm.prank(alice); + vm.expectEmit(true, true, true, true, hostIt); + emit TicketRefunded(ticketId, FeeType.USDT, fee, bob); + marketplaceFacet.claimRefund(ticketId, FeeType.USDT, tokenId, bob); + + assertEq(ticket.ownerOf(tokenId), owner); + assertEq(usdt.balanceOf(bob), fee); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), 0); + assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), hostItFee); + } + + function test_refundable_USDT_withdrawTicketBalanceAfterRefundPeriod() public { + (uint64 ticketId,, uint256 fee,, ERC20Mock usdt) = _mintTicketUSDTRefundable(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + + vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); + vm.expectEmit(true, true, true, true, hostIt); + emit TicketBalanceWithdrawn(ticketId, FeeType.USDT, fee, withdrawer); + marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.USDT, withdrawer); + + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), 0); + assertEq(usdt.balanceOf(withdrawer), fee); + } + + // ====================================================================== + // REFUNDABLE ESCROW: USDC + // ====================================================================== + + function test_refundable_USDC_fundsEscrowed() public { + (uint64 ticketId,, uint256 fee, uint256 hostItFee,) = _mintTicketUSDCRefundable(); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), fee); + assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), hostItFee); + } + + function test_refundable_USDC_organizerDoesNotReceiveFee() public { + (,,,, ERC20Mock usdc) = _mintTicketUSDCRefundable(); + assertEq(usdc.balanceOf(owner), 0); + } + + function test_refundable_USDC_contractHoldsTotalFee() public { + (,, uint256 fee, uint256 hostItFee, ERC20Mock usdc) = _mintTicketUSDCRefundable(); + assertEq(usdc.balanceOf(hostIt), fee + hostItFee); + } + + function test_refundable_USDC_claimRefund() public { + (uint64 ticketId, uint40 tokenId, uint256 fee, uint256 hostItFee, ERC20Mock usdc) = _mintTicketUSDCRefundable(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(fullTicketData.ticketAddress); + + vm.prank(alice); + ticket.approve(hostIt, tokenId); + + vm.warp(fullTicketData.endTime); + vm.prank(alice); + vm.expectEmit(true, true, true, true, hostIt); + emit TicketRefunded(ticketId, FeeType.USDC, fee, bob); + marketplaceFacet.claimRefund(ticketId, FeeType.USDC, tokenId, bob); + + assertEq(ticket.ownerOf(tokenId), owner); + assertEq(usdc.balanceOf(bob), fee); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), 0); + assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), hostItFee); + } + + function test_refundable_USDC_withdrawTicketBalanceAfterRefundPeriod() public { + (uint64 ticketId,, uint256 fee,, ERC20Mock usdc) = _mintTicketUSDCRefundable(); + FullTicketData memory fullTicketData = factoryFacet.ticketData(ticketId); + + vm.warp(fullTicketData.endTime + marketplaceFacet.getRefundPeriod()); + vm.expectEmit(true, true, true, true, hostIt); + emit TicketBalanceWithdrawn(ticketId, FeeType.USDC, fee, withdrawer); + marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.USDC, withdrawer); + + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDC), 0); + assertEq(usdc.balanceOf(withdrawer), fee); + } + + // ====================================================================== + // REFUNDABLE: HOSTIT BALANCE WITHDRAW AFTER ESCROW + // ====================================================================== + + function test_refundable_withdrawHostItBalanceETH() public { + (,,, uint256 hostItFee) = _mintTicketETHRefundable(); + vm.expectEmit(true, true, true, true, hostIt); + emit HostItBalanceWithdrawn(FeeType.ETH, hostItFee, withdrawer); + marketplaceFacet.withdrawHostItBalance(FeeType.ETH, withdrawer); + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), 0); + assertEq(withdrawer.balance, hostItFee); + } + + function test_refundable_withdrawHostItBalanceUSDT() public { + (,,, uint256 hostItFee, ERC20Mock usdt) = _mintTicketUSDTRefundable(); + vm.expectEmit(true, true, true, true, hostIt); + emit HostItBalanceWithdrawn(FeeType.USDT, hostItFee, withdrawer); + marketplaceFacet.withdrawHostItBalance(FeeType.USDT, withdrawer); + assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), 0); + assertEq(usdt.balanceOf(withdrawer), hostItFee); + } + + function test_refundable_withdrawHostItBalanceUSDC() public { + (,,, uint256 hostItFee, ERC20Mock usdc) = _mintTicketUSDCRefundable(); + vm.expectEmit(true, true, true, true, hostIt); + emit HostItBalanceWithdrawn(FeeType.USDC, hostItFee, withdrawer); + marketplaceFacet.withdrawHostItBalance(FeeType.USDC, withdrawer); + assertEq(marketplaceFacet.getHostItBalance(FeeType.USDC), 0); + assertEq(usdc.balanceOf(withdrawer), hostItFee); + } + + // ====================================================================== + // FEE CONFIGURATION + // ====================================================================== + + function test_setTicketFees() public { + _createFreeTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + marketplaceFacet.setTicketFees(ticketId, _getFeeTypes(), _getFees()); + assertEq(marketplaceFacet.getTicketFee(ticketId, FeeType.ETH), _getFees()[0]); + assertEq(marketplaceFacet.getTicketFee(ticketId, FeeType.USDT), _getFees()[1]); + assertEq(marketplaceFacet.getTicketFee(ticketId, FeeType.USDC), _getFees()[2]); + } + + function test_feeCalculation() public { + _createPaidTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + (uint256 fee, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + assertEq(fee, ETH_FEE); + assertEq(hostItFee, (ETH_FEE * 300) / 10_000); + assertEq(totalFee, fee + hostItFee); + } + + function test_feeNotEnabled_reverts() public { + _createPaidTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + hoax(alice, 1 ether); + vm.expectRevert(FeeNotEnabled.selector); + marketplaceFacet.mintTicket{value: 1 ether}(ticketId, FeeType.WETH, alice); + } + + // ====================================================================== + // MIXED: REFUNDABLE + NON-REFUNDABLE HOSTIT ACCUMULATION + // ====================================================================== + + function test_hostItBalanceAccumulatesAcrossTickets() public { + // Non-refundable ticket + (,,, uint256 hostItFee1) = _mintTicketETH(); + // Refundable ticket + (,,, uint256 hostItFee2) = _mintTicketETHRefundable(); + + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee1 + hostItFee2); + } + receive() external payable {} } diff --git a/test/states/DeployedHostItTickets.sol b/test/states/DeployedHostItTickets.sol index c363103..7ac27d3 100644 --- a/test/states/DeployedHostItTickets.sol +++ b/test/states/DeployedHostItTickets.sol @@ -178,6 +178,62 @@ abstract contract DeployedHostItTickets is Test, DeployHostItTicketsHelper { hostItFee_ = hostItFee; } + /// forge-lint: disable-next-line(mixed-case-function) + function _mintTicketETHRefundable() + internal + returns (uint64 ticketId_, uint40 tokenId_, uint256 fee_, uint256 hostItFee_) + { + _createRefundablePaidTicket(); + ticketId_ = factoryFacet.ticketCount(); + (uint256 fee, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId_, FeeType.ETH); + hoax(alice, totalFee); + vm.expectEmit(true, true, true, true, hostIt); + emit TicketMinted(ticketId_, FeeType.ETH, totalFee, 1); + tokenId_ = marketplaceFacet.mintTicket{value: totalFee}(ticketId_, FeeType.ETH, alice); + fee_ = fee; + hostItFee_ = hostItFee; + } + + /// forge-lint: disable-next-line(mixed-case-function) + function _mintTicketUSDTRefundable() + internal + returns (uint64 ticketId_, uint40 tokenId_, uint256 fee_, uint256 hostItFee_, ERC20Mock usdt_) + { + _createRefundablePaidTicket(); + ticketId_ = factoryFacet.ticketCount(); + (uint256 fee, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId_, FeeType.USDT); + usdt_ = ERC20Mock(marketplaceFacet.getFeeTokenAddress(FeeType.USDT)); + usdt_.mint(alice, totalFee); + vm.prank(alice); + usdt_.approve(address(marketplaceFacet), totalFee); + vm.prank(alice); + vm.expectEmit(true, true, true, true, hostIt); + emit TicketMinted(ticketId_, FeeType.USDT, totalFee, 1); + tokenId_ = marketplaceFacet.mintTicket(ticketId_, FeeType.USDT, alice); + fee_ = fee; + hostItFee_ = hostItFee; + } + + /// forge-lint: disable-next-line(mixed-case-function) + function _mintTicketUSDCRefundable() + internal + returns (uint64 ticketId_, uint40 tokenId_, uint256 fee_, uint256 hostItFee_, ERC20Mock usdc_) + { + _createRefundablePaidTicket(); + ticketId_ = factoryFacet.ticketCount(); + (uint256 fee, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId_, FeeType.USDC); + usdc_ = ERC20Mock(marketplaceFacet.getFeeTokenAddress(FeeType.USDC)); + usdc_.mint(alice, totalFee); + vm.prank(alice); + usdc_.approve(address(marketplaceFacet), totalFee); + vm.prank(alice); + vm.expectEmit(true, true, true, true, hostIt); + emit TicketMinted(ticketId_, FeeType.USDC, totalFee, 1); + tokenId_ = marketplaceFacet.mintTicket(ticketId_, FeeType.USDC, alice); + fee_ = fee; + hostItFee_ = hostItFee; + } + function _createFreeTicket() internal { factoryFacet.createTicket(_getFreeTicketData(), _getZeroFeeType(), _getZeroFee()); } @@ -190,6 +246,10 @@ abstract contract DeployedHostItTickets is Test, DeployHostItTicketsHelper { factoryFacet.createTicket(_getPaidTicketData(), _getFeeTypes(), _getFees()); } + function _createRefundablePaidTicket() internal { + factoryFacet.createTicket(_getRefundablePaidTicketData(), _getFeeTypes(), _getFees()); + } + function _updatePaidTicket(uint40 _ticketId) internal { factoryFacet.updateTicket(_getPaidUpdatedTicketData(), _ticketId); } @@ -239,6 +299,21 @@ abstract contract DeployedHostItTickets is Test, DeployHostItTicketsHelper { }); } + function _getRefundablePaidTicketData() internal view returns (TicketData memory ticketData_) { + ticketData_ = TicketData({ + startTime: uint40(block.timestamp + 1 days), + endTime: uint40(block.timestamp + 2 days), + purchaseStartTime: _currentTime, + maxTickets: type(uint40).max, + maxTicketsPerUser: 0, + isFree: false, + isRefundable: true, + name: "Refundable Paid Ticket", + symbol: "", + uri: "ipfs://refundable" + }); + } + function _getPaidUpdatedTicketData() internal view returns (TicketData memory ticketData_) { ticketData_ = TicketData({ startTime: uint40(block.timestamp + 1 days), From 1598434332e469418f9539736250600680cc32ff Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 3 Apr 2026 06:06:19 +0100 Subject: [PATCH 08/10] fmt: forge fmt --- script/helpers/DeployHostItTicketsHelper.sol | 4 +--- src/libs/LibMarketplace.sol | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/script/helpers/DeployHostItTicketsHelper.sol b/script/helpers/DeployHostItTicketsHelper.sol index cde4b73..d7e00f8 100644 --- a/script/helpers/DeployHostItTicketsHelper.sol +++ b/script/helpers/DeployHostItTicketsHelper.sol @@ -75,9 +75,7 @@ abstract contract DeployHostItTicketsHelper is GetSelectors, Context { function _getHostItTickets() internal returns (address) { return HOST_IT_TICKETS.code.length == 0 ? address( - new HostItTickets{ - salt: HOST_IT_SALT - }( + new HostItTickets{salt: HOST_IT_SALT}( _createInitFacetCuts(_getDiamondCutFacet(), _getDiamondLoupeFacet(), _getOwnableRolesFacet()), _getDiamondInit(), abi.encodeWithSignature("initDiamond(address)", _msgSender()) diff --git a/src/libs/LibMarketplace.sol b/src/libs/LibMarketplace.sol index c6da564..fdff203 100644 --- a/src/libs/LibMarketplace.sol +++ b/src/libs/LibMarketplace.sol @@ -229,9 +229,9 @@ library LibMarketplace { function _createErc6551Account(address _ticketAddress, uint256 _tokenId) internal { try IERC6551Registry(ERC6551_REGISTRY) - .createAccount( - ACCOUNT_V3_IMPLEMENTATION, "", block.chainid, _ticketAddress, _tokenId - ) returns (address account) { + .createAccount(ACCOUNT_V3_IMPLEMENTATION, "", block.chainid, _ticketAddress, _tokenId) returns ( + address account + ) { if (account == address(0)) { revert CreateERC6551AccountFailed(); } From cf24eb9c730a16258fe41e4e0a3bde04484ad9b6 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 3 Apr 2026 21:46:42 +0100 Subject: [PATCH 09/10] test: add foundry fuzz tests across all test files --- test/CheckIn.t.sol | 64 +++++++++ test/Factory.t.sol | 118 ++++++++++++--- test/Marketplace.t.sol | 317 ++++++++++++++++++++++++++++++++++++++++- test/Ticket.t.sol | 40 ++++++ 4 files changed, 519 insertions(+), 20 deletions(-) diff --git a/test/CheckIn.t.sol b/test/CheckIn.t.sol index bbc4b30..a49c47c 100644 --- a/test/CheckIn.t.sol +++ b/test/CheckIn.t.sol @@ -1,9 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.30; +import {FullTicketData} from "@ticket-storage/FactoryStorage.sol"; import {DeployedHostItTickets} from "@ticket-test/states/DeployedHostItTickets.sol"; /// forge-lint: disable-next-line(unaliased-plain-import) import "@ticket-logs/CheckInLogs.sol"; +/// forge-lint: disable-next-line(unaliased-plain-import) +import "@ticket-errors/CheckInErrors.sol"; contract CheckInTest is DeployedHostItTickets { function test_checkIn() public { @@ -54,4 +57,65 @@ contract CheckInTest is DeployedHostItTickets { vm.expectRevert(); checkInFacet.checkIn(ticketId, alice, 1); } + + // ====================================================================== + // FUZZ TESTS + // ====================================================================== + + function testFuzz_checkIn_validDay(uint256 dayOffset) public { + (uint64 ticketId, uint40 tokenId) = _mintTicketFree(); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + + // Event runs from startTime to endTime (1 day duration in default data) + uint256 eventDuration = ftd.endTime - ftd.startTime; + dayOffset = bound(dayOffset, 0, eventDuration - 1); + + vm.warp(ftd.startTime + dayOffset); + checkInFacet.checkIn(ticketId, alice, tokenId); + + uint8 expectedDay = uint8(dayOffset / 1 days); + assertTrue(checkInFacet.isCheckedIn(ticketId, alice)); + assertTrue(checkInFacet.isCheckedInForDay(ticketId, expectedDay, alice)); + } + + function testFuzz_checkIn_revertsBeforeStart(uint256 warpTo) public { + (uint64 ticketId, uint40 tokenId) = _mintTicketFree(); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + + warpTo = bound(warpTo, block.timestamp, ftd.startTime - 1); + vm.warp(warpTo); + + vm.expectRevert(TicketUsePeriodNotStarted.selector); + checkInFacet.checkIn(ticketId, alice, tokenId); + } + + function testFuzz_checkIn_revertsAfterEnd(uint256 extraTime) public { + (uint64 ticketId, uint40 tokenId) = _mintTicketFree(); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + + extraTime = bound(extraTime, 1, 365 days); + vm.warp(ftd.endTime + extraTime); + + vm.expectRevert(TicketUsePeriodHasEnded.selector); + checkInFacet.checkIn(ticketId, alice, tokenId); + } + + function testFuzz_addTicketAdmins(uint8 adminCount) public { + adminCount = uint8(bound(adminCount, 1, 20)); + + (uint64 ticketId,) = _mintTicketFree(); + + address[] memory admins = new address[](adminCount); + for (uint8 i; i < adminCount; ++i) { + admins[i] = makeAddr(string(abi.encodePacked("admin", i))); + } + + checkInFacet.addTicketAdmins(ticketId, admins); + + // Each admin can check in + vm.warp(1 days + 1); + vm.prank(admins[0]); + checkInFacet.checkIn(ticketId, alice, 1); + assertTrue(checkInFacet.isCheckedIn(ticketId, alice)); + } } diff --git a/test/Factory.t.sol b/test/Factory.t.sol index 8d09e0b..dddf7c0 100644 --- a/test/Factory.t.sol +++ b/test/Factory.t.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.30; import {TicketCreated, TicketUpdated} from "@ticket-logs/FactoryLogs.sol"; import {ExtraTicketData, FullTicketData, TicketData} from "@ticket-storage/FactoryStorage.sol"; import {DeployedHostItTickets} from "@ticket-test/states/DeployedHostItTickets.sol"; +/// forge-lint: disable-next-line(unaliased-plain-import) +import "@ticket-errors/FactoryErrors.sol"; contract FactoryTest is DeployedHostItTickets { function test_createFreeTicket() public { @@ -172,23 +174,101 @@ contract FactoryTest is DeployedHostItTickets { // FUZZ TESTS //////////////////////////////////////////////////////////////////////////*// - // /// forge-config: default.fuzz.runs = 20 - // /// forge-config: default.fuzz.max-test-rejects = 100_000_000 - // function test_fuzz_createTicket(TicketData memory _ticketData) public { - // vm.assume(_ticketData.endTime > bound(_ticketData.startTime, 0, type(uint48).max - 2 days)); - // bound(_ticketData.purchaseStartTime, 0, _currentTime); - // bound(_ticketData.maxTickets, 0, type(uint8).max); - // factoryFacet.createTicket(_ticketData, _getFeeTypes(), _getFees()); - // } - - // /// forge-config: default.fuzz.runs = 256 - // /// forge-config: default.fuzz.max-test-rejects = 4_000_000 - // function test_fuzz_updateTicket(TicketData memory _ticketData) public { - // bound(_ticketData.startTime, 0, type(uint48).max - 2 days); - // vm.assume(_ticketData.endTime == type(uint48).max - 1 days); - // bound(_ticketData.purchaseStartTime, 0, _currentTime); - // bound(_ticketData.maxTickets, 0, type(uint8).max); - // _createPaidTicket(); - // factoryFacet.updateTicket(_ticketData, 1); - // } + function testFuzz_createTicket_validTimes(uint48 startOffset, uint48 duration) public { + startOffset = uint48(bound(startOffset, 2 days, 365 days)); + duration = uint48(bound(duration, 1 days, 365 days)); + + uint48 startTime = uint48(block.timestamp) + startOffset; + uint48 endTime = startTime + duration; + uint48 purchaseStartTime = startTime - uint48(1 days); + + TicketData memory td = TicketData({ + startTime: startTime, + endTime: endTime, + purchaseStartTime: purchaseStartTime, + maxTickets: type(uint40).max, + maxTicketsPerUser: 0, + isFree: true, + isRefundable: false, + name: "Fuzz Ticket", + symbol: "", + uri: "ipfs://fuzz" + }); + + factoryFacet.createTicket(td, _getZeroFeeType(), _getZeroFee()); + uint64 ticketId = factoryFacet.ticketCount(); + assertTrue(factoryFacet.ticketExists(ticketId)); + + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + assertEq(ftd.startTime, startTime); + assertEq(ftd.endTime, endTime); + assertEq(ftd.purchaseStartTime, purchaseStartTime); + } + + function testFuzz_createTicket_revertsStartTimeInPast(uint48 offset) public { + offset = uint48(bound(offset, 1, uint48(block.timestamp))); + uint48 startTime = uint48(block.timestamp) - offset; + + TicketData memory td = TicketData({ + startTime: startTime, + endTime: startTime + uint48(2 days), + purchaseStartTime: 0, + maxTickets: type(uint40).max, + maxTicketsPerUser: 0, + isFree: true, + isRefundable: false, + name: "Bad Ticket", + symbol: "", + uri: "ipfs://bad" + }); + + vm.expectRevert(StartTimeShouldBeAhead.selector); + factoryFacet.createTicket(td, _getZeroFeeType(), _getZeroFee()); + } + + function testFuzz_createTicket_revertsEndTimeTooClose(uint48 gap) public { + gap = uint48(bound(gap, 0, 1 days - 1)); + uint48 startTime = uint48(block.timestamp + 2 days); + uint48 endTime = startTime + gap; + + TicketData memory td = TicketData({ + startTime: startTime, + endTime: endTime, + purchaseStartTime: uint48(block.timestamp), + maxTickets: type(uint40).max, + maxTicketsPerUser: 0, + isFree: true, + isRefundable: false, + name: "Bad Ticket", + symbol: "", + uri: "ipfs://bad" + }); + + vm.expectRevert(EndTimeShouldBeOneDayAfterStartTime.selector); + factoryFacet.createTicket(td, _getZeroFeeType(), _getZeroFee()); + } + + function testFuzz_ticketHash(uint64 ticketId) public view { + bytes32 ticketHash = factoryFacet.ticketHash(ticketId); + assertEq(ticketHash, keccak256(abi.encode(keccak256("host.it.ticket"), ticketId))); + } + + function testFuzz_mainAdminRole(uint64 ticketId) public view { + uint256 mainAdminRole = factoryFacet.mainAdminRole(ticketId); + assertEq(mainAdminRole, uint256(keccak256(abi.encode(keccak256("host.it.ticket.main.admin"), ticketId)))); + } + + function testFuzz_ticketAdminRole(uint64 ticketId) public view { + uint256 ticketAdminRole = factoryFacet.ticketAdminRole(ticketId); + assertEq(ticketAdminRole, uint256(keccak256(abi.encode(keccak256("host.it.ticket.admin"), ticketId)))); + } + + function testFuzz_ticketExists(uint64 ticketId) public { + _createFreeTicket(); + if (ticketId == 1) { + assertTrue(factoryFacet.ticketExists(ticketId)); + } else { + assertFalse(factoryFacet.ticketExists(ticketId)); + } + } } diff --git a/test/Marketplace.t.sol b/test/Marketplace.t.sol index 72c131c..f7993e3 100644 --- a/test/Marketplace.t.sol +++ b/test/Marketplace.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.30; import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import {FullTicketData} from "@ticket-storage/FactoryStorage.sol"; +import {FullTicketData, TicketData} from "@ticket-storage/FactoryStorage.sol"; import {FeeType} from "@ticket-storage/MarketplaceStorage.sol"; import {DeployedHostItTickets} from "@ticket-test/states/DeployedHostItTickets.sol"; import {ITicket} from "@ticket/interfaces/ITicket.sol"; @@ -575,5 +575,320 @@ contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee1 + hostItFee2); } + // ====================================================================== + // FUZZ TESTS + // ====================================================================== + + function testFuzz_hostItFeeCalculation(uint256 fee) public view { + fee = bound(fee, 0, type(uint256).max / 300); + uint256 hostItFee = marketplaceFacet.getHostItFee(fee); + assertEq(hostItFee, (fee * 300) / 10_000); + } + + function testFuzz_totalFeeIsSumOfParts(uint256 fee) public { + fee = bound(fee, 1, 1e30); + + _createFreeTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + + FeeType[] memory feeTypes = new FeeType[](1); + feeTypes[0] = FeeType.ETH; + uint256[] memory fees = new uint256[](1); + fees[0] = fee; + marketplaceFacet.setTicketFees(ticketId, feeTypes, fees); + + (uint256 ticketFee, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + assertEq(ticketFee, fee); + assertEq(hostItFee, (fee * 300) / 10_000); + assertEq(totalFee, ticketFee + hostItFee); + } + + function testFuzz_directPayment_ETH_accounting(uint256 fee) public { + fee = bound(fee, 1, 1e24); + + TicketData memory td = _getPaidTicketData(); + FeeType[] memory feeTypes = new FeeType[](1); + feeTypes[0] = FeeType.ETH; + uint256[] memory fees = new uint256[](1); + fees[0] = fee; + factoryFacet.createTicket(td, feeTypes, fees); + uint64 ticketId = factoryFacet.ticketCount(); + + (uint256 ticketFee, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + + uint256 ownerBalBefore = owner.balance; + uint256 contractBalBefore = hostIt.balance; + + hoax(alice, totalFee); + marketplaceFacet.mintTicket{value: totalFee}(ticketId, FeeType.ETH, alice); + + assertEq(owner.balance - ownerBalBefore, ticketFee); + assertEq(hostIt.balance - contractBalBefore, hostItFee); + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); + assertEq(alice.balance, 0); + } + + function testFuzz_directPayment_USDT_accounting(uint256 fee) public { + fee = bound(fee, 1, 1e30); + + TicketData memory td = _getPaidTicketData(); + FeeType[] memory feeTypes = new FeeType[](1); + feeTypes[0] = FeeType.USDT; + uint256[] memory fees = new uint256[](1); + fees[0] = fee; + factoryFacet.createTicket(td, feeTypes, fees); + uint64 ticketId = factoryFacet.ticketCount(); + + (uint256 ticketFee, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.USDT); + ERC20Mock usdt = ERC20Mock(marketplaceFacet.getFeeTokenAddress(FeeType.USDT)); + usdt.mint(alice, totalFee); + + vm.prank(alice); + usdt.approve(address(marketplaceFacet), totalFee); + vm.prank(alice); + marketplaceFacet.mintTicket(ticketId, FeeType.USDT, alice); + + assertEq(usdt.balanceOf(owner), ticketFee); + assertEq(usdt.balanceOf(hostIt), hostItFee); + assertEq(marketplaceFacet.getHostItBalance(FeeType.USDT), hostItFee); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.USDT), 0); + assertEq(usdt.balanceOf(alice), 0); + } + + function testFuzz_refundable_ETH_escrow(uint256 fee) public { + fee = bound(fee, 1, 1e24); + + TicketData memory td = _getRefundablePaidTicketData(); + FeeType[] memory feeTypes = new FeeType[](1); + feeTypes[0] = FeeType.ETH; + uint256[] memory fees = new uint256[](1); + fees[0] = fee; + factoryFacet.createTicket(td, feeTypes, fees); + uint64 ticketId = factoryFacet.ticketCount(); + + (uint256 ticketFee, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + + uint256 ownerBalBefore = owner.balance; + uint256 contractBalBefore = hostIt.balance; + + hoax(alice, totalFee); + marketplaceFacet.mintTicket{value: totalFee}(ticketId, FeeType.ETH, alice); + + assertEq(owner.balance, ownerBalBefore); + assertEq(hostIt.balance - contractBalBefore, ticketFee + hostItFee); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), ticketFee); + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); + } + + function testFuzz_refundable_ETH_claimWithinWindow(uint256 warpOffset) public { + (uint64 ticketId, uint40 tokenId, uint256 fee, uint256 hostItFee) = _mintTicketETHRefundable(); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(ftd.ticketAddress); + + vm.prank(alice); + ticket.approve(hostIt, tokenId); + + uint256 refundPeriod = marketplaceFacet.getRefundPeriod(); + warpOffset = bound(warpOffset, 0, refundPeriod); + vm.warp(ftd.endTime + warpOffset); + + vm.prank(alice); + marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, bob); + + assertEq(bob.balance, fee); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); + } + + function testFuzz_refundable_ETH_claimRevertsBeforeEndTime(uint256 warpTo) public { + (uint64 ticketId, uint40 tokenId,,) = _mintTicketETHRefundable(); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(ftd.ticketAddress); + + vm.prank(alice); + ticket.approve(hostIt, tokenId); + + warpTo = bound(warpTo, block.timestamp, ftd.endTime - 1); + vm.warp(warpTo); + + vm.prank(alice); + vm.expectRevert(RefundPeriodNotReached.selector); + marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, bob); + } + + function testFuzz_refundable_ETH_claimRevertsAfterWindow(uint256 extraTime) public { + (uint64 ticketId, uint40 tokenId,,) = _mintTicketETHRefundable(); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(ftd.ticketAddress); + + vm.prank(alice); + ticket.approve(hostIt, tokenId); + + uint256 refundPeriod = marketplaceFacet.getRefundPeriod(); + extraTime = bound(extraTime, 1, 365 days); + vm.warp(ftd.endTime + refundPeriod + extraTime); + + vm.prank(alice); + vm.expectRevert(RefundPeriodExpired.selector); + marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, bob); + } + + function testFuzz_refundable_ETH_withdrawAfterRefundPeriod(uint256 extraTime) public { + (uint64 ticketId,, uint256 fee,) = _mintTicketETHRefundable(); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + + uint256 refundPeriod = marketplaceFacet.getRefundPeriod(); + extraTime = bound(extraTime, 0, 365 days); + vm.warp(ftd.endTime + refundPeriod + extraTime); + + marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.ETH, withdrawer); + assertEq(withdrawer.balance, fee); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); + } + + function testFuzz_refundable_ETH_withdrawRevertsBeforeRefundPeriod(uint256 warpTo) public { + (uint64 ticketId,,,) = _mintTicketETHRefundable(); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + + uint256 refundPeriod = marketplaceFacet.getRefundPeriod(); + warpTo = bound(warpTo, block.timestamp, ftd.endTime + refundPeriod - 1); + vm.warp(warpTo); + + vm.expectRevert(WithdrawPeriodNotReached.selector); + marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.ETH, withdrawer); + } + + function testFuzz_directPayment_ETH_multipleBuyersAccumulate(uint8 buyerCount) public { + buyerCount = uint8(bound(buyerCount, 1, 20)); + + _createPaidTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + (uint256 ticketFee, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + + uint256 ownerBalBefore = owner.balance; + + for (uint8 i; i < buyerCount; ++i) { + address buyer = makeAddr(string(abi.encodePacked("buyer", i))); + hoax(buyer, totalFee); + marketplaceFacet.mintTicket{value: totalFee}(ticketId, FeeType.ETH, buyer); + } + + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee * buyerCount); + assertEq(owner.balance - ownerBalBefore, ticketFee * buyerCount); + + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + assertEq(ftd.soldTickets, buyerCount); + } + + function testFuzz_directPayment_ETH_revertsInsufficientValue(uint256 underpay) public { + _createPaidTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + (,, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + + underpay = bound(underpay, 1, totalFee); + uint256 sent = totalFee - underpay; + + hoax(alice, totalFee); + vm.expectRevert(abi.encodeWithSelector(TicketPurchaseFailed.selector, FeeType.ETH, totalFee)); + marketplaceFacet.mintTicket{value: sent}(ticketId, FeeType.ETH, alice); + } + + function testFuzz_withdrawHostItBalance_ETH(uint256 fee) public { + // fee must be >= 34 so hostItFee = fee * 300 / 10_000 > 0 + fee = bound(fee, 34, 1e24); + + TicketData memory td = _getPaidTicketData(); + FeeType[] memory feeTypes = new FeeType[](1); + feeTypes[0] = FeeType.ETH; + uint256[] memory fees = new uint256[](1); + fees[0] = fee; + factoryFacet.createTicket(td, feeTypes, fees); + uint64 ticketId = factoryFacet.ticketCount(); + + (, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + + hoax(alice, totalFee); + marketplaceFacet.mintTicket{value: totalFee}(ticketId, FeeType.ETH, alice); + + marketplaceFacet.withdrawHostItBalance(FeeType.ETH, withdrawer); + assertEq(withdrawer.balance, hostItFee); + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), 0); + } + + function testFuzz_refundable_ETH_fullLifecycle(uint256 fee, uint256 refundOffset) public { + fee = bound(fee, 1, 1e24); + uint256 refundPeriod = marketplaceFacet.getRefundPeriod(); + refundOffset = bound(refundOffset, 0, refundPeriod); + + TicketData memory td = _getRefundablePaidTicketData(); + FeeType[] memory feeTypes = new FeeType[](1); + feeTypes[0] = FeeType.ETH; + uint256[] memory fees = new uint256[](1); + fees[0] = fee; + factoryFacet.createTicket(td, feeTypes, fees); + uint64 ticketId = factoryFacet.ticketCount(); + + (uint256 ticketFee, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(ftd.ticketAddress); + + hoax(alice, totalFee); + uint40 tokenId = marketplaceFacet.mintTicket{value: totalFee}(ticketId, FeeType.ETH, alice); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), ticketFee); + + vm.prank(alice); + ticket.approve(hostIt, tokenId); + vm.warp(ftd.endTime + refundOffset); + + vm.prank(alice); + marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenId, bob); + + assertEq(bob.balance, ticketFee); + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), 0); + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee); + assertEq(ticket.ownerOf(tokenId), owner); + } + + function testFuzz_refundable_ETH_multipleBuyersPartialRefund(uint8 buyerCount, uint8 refundCount) public { + buyerCount = uint8(bound(buyerCount, 2, 10)); + refundCount = uint8(bound(refundCount, 1, buyerCount - 1)); + + TicketData memory td = _getRefundablePaidTicketData(); + FeeType[] memory feeTypes = new FeeType[](1); + feeTypes[0] = FeeType.ETH; + uint256[] memory fees = new uint256[](1); + fees[0] = ETH_FEE; + factoryFacet.createTicket(td, feeTypes, fees); + uint64 ticketId = factoryFacet.ticketCount(); + + (uint256 ticketFee, uint256 hostItFee, uint256 totalFee) = marketplaceFacet.getAllFees(ticketId, FeeType.ETH); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + ITicket ticket = ITicket(ftd.ticketAddress); + + address[] memory buyers = new address[](buyerCount); + uint40[] memory tokenIds = new uint40[](buyerCount); + for (uint8 i; i < buyerCount; ++i) { + buyers[i] = makeAddr(string(abi.encodePacked("rbuyer", i))); + hoax(buyers[i], totalFee); + tokenIds[i] = marketplaceFacet.mintTicket{value: totalFee}(ticketId, FeeType.ETH, buyers[i]); + } + + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), ticketFee * buyerCount); + + vm.warp(ftd.endTime); + + for (uint8 i; i < refundCount; ++i) { + vm.prank(buyers[i]); + ticket.approve(hostIt, tokenIds[i]); + vm.prank(buyers[i]); + marketplaceFacet.claimRefund(ticketId, FeeType.ETH, tokenIds[i], buyers[i]); + } + + uint256 remaining = uint256(buyerCount - refundCount) * ticketFee; + assertEq(marketplaceFacet.getTicketBalance(ticketId, FeeType.ETH), remaining); + assertEq(marketplaceFacet.getHostItBalance(FeeType.ETH), hostItFee * buyerCount); + } + receive() external payable {} } diff --git a/test/Ticket.t.sol b/test/Ticket.t.sol index 48975ca..43a9c34 100644 --- a/test/Ticket.t.sol +++ b/test/Ticket.t.sol @@ -146,4 +146,44 @@ contract TicketTest is Test { assertEq(ticketClone2.baseURI(), "ipfs://2"); assertNotEq(address(ticketClone), address(ticketClone2)); } + + // ====================================================================== + // FUZZ TESTS + // ====================================================================== + + function testFuzz_mint(uint8 count) public { + count = uint8(bound(count, 1, 50)); + for (uint8 i; i < count; ++i) { + ticketClone.mint(alice); + } + assertEq(ticketClone.totalSupply(), count); + assertEq(ticketClone.balanceOf(alice), count); + } + + function testFuzz_updateName(string calldata name) public { + vm.assume(bytes(name).length > 0); + ticketClone.updateName(name); + assertEq(ticketClone.name(), name); + } + + function testFuzz_updateSymbol(string calldata symbol) public { + ticketClone.updateSymbol(symbol); + assertEq(ticketClone.symbol(), symbol); + } + + function testFuzz_updateURI(string calldata uri) public { + ticketClone.updateURI(uri); + assertEq(ticketClone.baseURI(), uri); + } + + /// forge-lint: disable-next-item(erc20-unchecked-transfer) + function testFuzz_transferBetweenAddresses(address to) public { + vm.assume(to != address(0) && to.code.length == 0 && to != alice); + uint256 tokenId = ticketClone.mint(alice); + vm.prank(alice); + ticketClone.transferFrom(alice, to, tokenId); + assertEq(ticketClone.ownerOf(tokenId), to); + assertEq(ticketClone.balanceOf(to), 1); + assertEq(ticketClone.balanceOf(alice), 0); + } } From b1bfe291383fc398fbb90ded0cd8d96328c3f70f Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 3 Apr 2026 22:51:22 +0100 Subject: [PATCH 10/10] test: increase coverage with revert and edge case tests --- test/CheckIn.t.sol | 103 ++++++++++++++++++++++++++++++ test/Factory.t.sol | 133 +++++++++++++++++++++++++++++++++++++++ test/Marketplace.t.sol | 139 +++++++++++++++++++++++++++++++++++++++++ test/Ticket.t.sol | 107 +++++++++++++++++++++++++++++++ 4 files changed, 482 insertions(+) diff --git a/test/CheckIn.t.sol b/test/CheckIn.t.sol index a49c47c..7fffcfa 100644 --- a/test/CheckIn.t.sol +++ b/test/CheckIn.t.sol @@ -58,6 +58,109 @@ contract CheckInTest is DeployedHostItTickets { checkInFacet.checkIn(ticketId, alice, 1); } + // ====================================================================== + // REVERT TESTS + // ====================================================================== + + function test_checkIn_revertsNotTicketOwner() public { + (uint64 ticketId,) = _mintTicketFree(); + vm.warp(1 days + 1); + vm.expectRevert(abi.encodeWithSelector(NotTicketOwner.selector, uint40(1))); + checkInFacet.checkIn(ticketId, bob, 1); + } + + function test_checkIn_revertsAlreadyCheckedInForDay() public { + (uint64 ticketId, uint40 tokenId) = _mintTicketFree(); + vm.warp(1 days + 1); + checkInFacet.checkIn(ticketId, alice, tokenId); + vm.expectRevert(abi.encodeWithSelector(AlreadyCheckedInForDay.selector, uint8(0))); + checkInFacet.checkIn(ticketId, alice, tokenId); + } + + function test_addTicketAdmins_revertsNoAdmins() public { + (uint64 ticketId,) = _mintTicketFree(); + address[] memory admins = new address[](0); + vm.expectRevert(NoAdmins.selector); + checkInFacet.addTicketAdmins(ticketId, admins); + } + + function test_addTicketAdmins_revertsAddressZero() public { + (uint64 ticketId,) = _mintTicketFree(); + address[] memory admins = new address[](1); + admins[0] = address(0); + vm.expectRevert(AddressZeroAdmin.selector); + checkInFacet.addTicketAdmins(ticketId, admins); + } + + function test_removeTicketAdmins_revertsNoAdmins() public { + (uint64 ticketId,) = _mintTicketFree(); + address[] memory admins = new address[](0); + vm.expectRevert(NoAdmins.selector); + checkInFacet.removeTicketAdmins(ticketId, admins); + } + + function test_removeTicketAdmins_revertsAddressZero() public { + (uint64 ticketId,) = _mintTicketFree(); + address[] memory admins = new address[](1); + admins[0] = address(0); + vm.expectRevert(AddressZeroAdmin.selector); + checkInFacet.removeTicketAdmins(ticketId, admins); + } + + function test_addTicketAdmins_revertsNonMainAdmin() public { + (uint64 ticketId,) = _mintTicketFree(); + address[] memory admins = new address[](1); + admins[0] = charlie; + vm.prank(alice); + vm.expectRevert(); + checkInFacet.addTicketAdmins(ticketId, admins); + } + + function test_removeTicketAdmins_revertsNonMainAdmin() public { + (uint64 ticketId,) = _mintTicketFree(); + address[] memory admins = new address[](1); + admins[0] = bob; + checkInFacet.addTicketAdmins(ticketId, admins); + vm.prank(alice); + vm.expectRevert(); + checkInFacet.removeTicketAdmins(ticketId, admins); + } + + function test_checkIn_revertsNonAdmin() public { + (uint64 ticketId, uint40 tokenId) = _mintTicketFree(); + vm.warp(1 days + 1); + vm.prank(alice); + vm.expectRevert(); + checkInFacet.checkIn(ticketId, alice, tokenId); + } + + function test_getCheckedIn() public { + (uint64 ticketId, uint40 tokenId) = _mintTicketFree(); + vm.warp(1 days + 1); + checkInFacet.checkIn(ticketId, alice, tokenId); + address[] memory checkedIn = checkInFacet.getCheckedIn(ticketId); + assertEq(checkedIn.length, 1); + assertEq(checkedIn[0], alice); + } + + function test_getCheckedInForDay() public { + (uint64 ticketId, uint40 tokenId) = _mintTicketFree(); + vm.warp(1 days + 1); + checkInFacet.checkIn(ticketId, alice, tokenId); + address[] memory checkedIn = checkInFacet.getCheckedInForDay(ticketId, 0); + assertEq(checkedIn.length, 1); + assertEq(checkedIn[0], alice); + // Day 1 should be empty + address[] memory day1 = checkInFacet.getCheckedInForDay(ticketId, 1); + assertEq(day1.length, 0); + } + + function test_isCheckedIn_returnsFalseWhenNot() public { + (uint64 ticketId,) = _mintTicketFree(); + assertFalse(checkInFacet.isCheckedIn(ticketId, alice)); + assertFalse(checkInFacet.isCheckedInForDay(ticketId, 0, alice)); + } + // ====================================================================== // FUZZ TESTS // ====================================================================== diff --git a/test/Factory.t.sol b/test/Factory.t.sol index dddf7c0..1344c48 100644 --- a/test/Factory.t.sol +++ b/test/Factory.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.30; import {TicketCreated, TicketUpdated} from "@ticket-logs/FactoryLogs.sol"; import {ExtraTicketData, FullTicketData, TicketData} from "@ticket-storage/FactoryStorage.sol"; +import {FeeType} from "@ticket-storage/MarketplaceStorage.sol"; import {DeployedHostItTickets} from "@ticket-test/states/DeployedHostItTickets.sol"; /// forge-lint: disable-next-line(unaliased-plain-import) import "@ticket-errors/FactoryErrors.sol"; @@ -170,6 +171,138 @@ contract FactoryTest is DeployedHostItTickets { assertEq(ticketAdminRole, uint256(keccak256(abi.encode(keccak256("host.it.ticket.admin"), ticketId)))); } + //*////////////////////////////////////////////////////////////////////////// + // REVERT TESTS + //////////////////////////////////////////////////////////////////////////*// + + function test_createTicket_revertsEmptyName() public { + TicketData memory td = _getFreeTicketData(); + td.name = ""; + vm.expectRevert(EmptyName.selector); + factoryFacet.createTicket(td, _getZeroFeeType(), _getZeroFee()); + } + + function test_createTicket_revertsEmptyURI() public { + TicketData memory td = _getFreeTicketData(); + td.uri = ""; + vm.expectRevert(EmptyURI.selector); + factoryFacet.createTicket(td, _getZeroFeeType(), _getZeroFee()); + } + + function test_createTicket_revertsMaxTicketsIsZero() public { + TicketData memory td = _getFreeTicketData(); + td.maxTickets = 0; + vm.expectRevert(MaxTicketsIsZero.selector); + factoryFacet.createTicket(td, _getZeroFeeType(), _getZeroFee()); + } + + function test_createTicket_revertsPurchaseStartTimeTooLate() public { + TicketData memory td = _getFreeTicketData(); + // purchaseStartTime must be <= startTime - 1 day + td.purchaseStartTime = td.startTime; + vm.expectRevert(PurchaseStartTimeShouldBeOneDayBeforeStartTime.selector); + factoryFacet.createTicket(td, _getZeroFeeType(), _getZeroFee()); + } + + function test_createPaidTicket_revertsArrayMismatch() public { + TicketData memory td = _getPaidTicketData(); + // Empty feeTypes but paid ticket + vm.expectRevert(ArrayMismatch.selector); + factoryFacet.createTicket(td, _getZeroFeeType(), _getZeroFee()); + } + + function test_createPaidTicket_revertsArrayLengthMismatch() public { + TicketData memory td = _getPaidTicketData(); + FeeType[] memory feeTypes = new FeeType[](2); + feeTypes[0] = FeeType.ETH; + feeTypes[1] = FeeType.USDT; + uint256[] memory fees = new uint256[](1); + fees[0] = ETH_FEE; + vm.expectRevert(ArrayMismatch.selector); + factoryFacet.createTicket(td, feeTypes, fees); + } + + function test_createPaidTicket_revertsZeroFee() public { + TicketData memory td = _getPaidTicketData(); + FeeType[] memory feeTypes = new FeeType[](1); + feeTypes[0] = FeeType.ETH; + uint256[] memory fees = new uint256[](1); + fees[0] = 0; + vm.expectRevert(abi.encodeWithSelector(ZeroFee.selector, FeeType.ETH)); + factoryFacet.createTicket(td, feeTypes, fees); + } + + function test_createPaidTicket_revertsDuplicateFeeType() public { + TicketData memory td = _getPaidTicketData(); + FeeType[] memory feeTypes = new FeeType[](2); + feeTypes[0] = FeeType.ETH; + feeTypes[1] = FeeType.ETH; + uint256[] memory fees = new uint256[](2); + fees[0] = ETH_FEE; + fees[1] = ETH_FEE; + vm.expectRevert(abi.encodeWithSelector(FeeAlreadySet.selector, FeeType.ETH)); + factoryFacet.createTicket(td, feeTypes, fees); + } + + function test_createTicket_revertsNonAdminCannotUpdate() public { + _createFreeTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + TicketData memory td = _getFreeUpdatedTicketData(); + vm.prank(alice); + vm.expectRevert(); + factoryFacet.updateTicket(td, ticketId); + } + + function test_updateTicket_revertsTicketDoesNotExist() public { + TicketData memory td = _getFreeUpdatedTicketData(); + vm.expectRevert(abi.encodeWithSelector(TicketDoesNotExist.selector, uint64(999))); + factoryFacet.updateTicket(td, 999); + } + + function test_updateTicket_revertsEndTimeTooClose() public { + _createFreeTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + TicketData memory td = _getFreeUpdatedTicketData(); + td.endTime = td.startTime; // Less than startTime + 1 day + vm.expectRevert(EndTimeShouldBeOneDayAfterStartTime.selector); + factoryFacet.updateTicket(td, ticketId); + } + + function test_updateTicket_revertsPurchaseStartTimeTooLate() public { + _createFreeTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + TicketData memory td = _getFreeUpdatedTicketData(); + td.purchaseStartTime = td.startTime; // Must be <= startTime - 1 day + vm.expectRevert(PurchaseStartTimeShouldBeOneDayBeforeStartTime.selector); + factoryFacet.updateTicket(td, ticketId); + } + + function test_updateTicket_revertsMaxTicketsBelowSupply() public { + _createFreeTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + // Mint a ticket so supply > 0 + marketplaceFacet.mintTicket(ticketId, FeeType.NONE, alice); + // Try to set maxTickets to 0 (below supply of 1) + // maxTickets > 0 triggers the check, and totalSupply() == 1 + TicketData memory td = _getFreeUpdatedTicketData(); + // We need maxTickets > 0 but < totalSupply which is impossible with uint40 + // Let's mint to get supply then try to lower max + // Actually we need maxTickets < totalSupply. Mint one, then update maxTickets to... well 0 won't trigger the branch. + // The condition is: _ticketData.maxTickets > 0 AND _ticketData.maxTickets < ticket.totalSupply() + // We can't hit this since totalSupply=1 and maxTickets must be uint40 >= 1 + // BUT if we mint multiple... + marketplaceFacet.mintTicket(ticketId, FeeType.NONE, bob); + // Now totalSupply = 2. Setting maxTickets to 1 should revert + td.maxTickets = 1; + vm.expectRevert(MaxTicketsShouldEqualSupply.selector); + factoryFacet.updateTicket(td, ticketId); + } + + function test_ticketData_revertsTicketDoesNotExist() public { + vm.expectRevert(abi.encodeWithSelector(TicketDoesNotExist.selector, uint64(999))); + factoryFacet.ticketData(999); + } + //*////////////////////////////////////////////////////////////////////////// // FUZZ TESTS //////////////////////////////////////////////////////////////////////////*// diff --git a/test/Marketplace.t.sol b/test/Marketplace.t.sol index f7993e3..8a195f0 100644 --- a/test/Marketplace.t.sol +++ b/test/Marketplace.t.sol @@ -562,6 +562,145 @@ contract MarketplaceTest is DeployedHostItTickets, ERC721Holder { marketplaceFacet.mintTicket{value: 1 ether}(ticketId, FeeType.WETH, alice); } + // ====================================================================== + // TICKET SOLD OUT + // ====================================================================== + + function test_mintTicket_revertsTicketSoldOut() public { + TicketData memory td = _getFreeTicketData(); + td.maxTickets = 1; + factoryFacet.createTicket(td, _getZeroFeeType(), _getZeroFee()); + uint64 ticketId = factoryFacet.ticketCount(); + marketplaceFacet.mintTicket(ticketId, FeeType.NONE, alice); + vm.expectRevert(TicketSoldOut.selector); + marketplaceFacet.mintTicket(ticketId, FeeType.NONE, bob); + } + + // ====================================================================== + // MAX TICKETS PER USER + // ====================================================================== + + function test_mintTicket_revertsMaxTicketsHeld() public { + TicketData memory td = _getFreeTicketData(); + td.maxTicketsPerUser = 1; + factoryFacet.createTicket(td, _getZeroFeeType(), _getZeroFee()); + uint64 ticketId = factoryFacet.ticketCount(); + // Mint first ticket (balance becomes 1, check is > maxTicketsPerUser so 1 > 1 = false, ok) + marketplaceFacet.mintTicket(ticketId, FeeType.NONE, alice); + // Second mint: balance is 1, but then check is 1 > 1 = false. Need to check logic... + // Actually the check is: if (ticket.balanceOf(_buyer) > ticketData.maxTicketsPerUser) revert + // After first mint, balance = 1. maxTicketsPerUser = 1. 1 > 1 = false. So need to mint again. + marketplaceFacet.mintTicket(ticketId, FeeType.NONE, alice); + // Now balance = 2. 2 > 1 = true. Revert. + vm.expectRevert(MaxTicketsHeld.selector); + marketplaceFacet.mintTicket(ticketId, FeeType.NONE, alice); + } + + // ====================================================================== + // PURCHASE TIME CHECKS + // ====================================================================== + + function test_mintTicket_revertsPurchaseTimeNotReached() public { + TicketData memory td = _getFreeTicketData(); + td.purchaseStartTime = uint48(block.timestamp + 1 days); + td.startTime = uint48(block.timestamp + 2 days); + td.endTime = uint48(block.timestamp + 3 days); + factoryFacet.createTicket(td, _getZeroFeeType(), _getZeroFee()); + uint64 ticketId = factoryFacet.ticketCount(); + vm.expectRevert(PurchaseTimeNotReached.selector); + marketplaceFacet.mintTicket(ticketId, FeeType.NONE, alice); + } + + function test_mintTicket_revertsAfterEventEnded() public { + _createFreeTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + vm.warp(ftd.endTime + 1); + vm.expectRevert(PurchaseTimeNotReached.selector); + marketplaceFacet.mintTicket(ticketId, FeeType.NONE, alice); + } + + // ====================================================================== + // SET TICKET FEES: REVERT CASES + // ====================================================================== + + function test_setTicketFees_revertsZeroFee() public { + _createFreeTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + FeeType[] memory feeTypes = new FeeType[](1); + feeTypes[0] = FeeType.ETH; + uint256[] memory fees = new uint256[](1); + fees[0] = 0; + vm.expectRevert(ZeroFee.selector); + marketplaceFacet.setTicketFees(ticketId, feeTypes, fees); + } + + function test_setTicketFees_revertsFeeAlreadySet() public { + _createFreeTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + FeeType[] memory feeTypes = new FeeType[](1); + feeTypes[0] = FeeType.ETH; + uint256[] memory fees = new uint256[](1); + fees[0] = ETH_FEE; + marketplaceFacet.setTicketFees(ticketId, feeTypes, fees); + vm.expectRevert(FeeAlreadySet.selector); + marketplaceFacet.setTicketFees(ticketId, feeTypes, fees); + } + + function test_setTicketFees_revertsInvalidFeeConfig() public { + _createFreeTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + FeeType[] memory feeTypes = new FeeType[](2); + feeTypes[0] = FeeType.ETH; + feeTypes[1] = FeeType.USDT; + uint256[] memory fees = new uint256[](1); + fees[0] = ETH_FEE; + vm.expectRevert(InvalidFeeConfig.selector); + marketplaceFacet.setTicketFees(ticketId, feeTypes, fees); + } + + function test_setTicketFees_revertsNonAdmin() public { + _createFreeTicket(); + uint64 ticketId = factoryFacet.ticketCount(); + vm.prank(alice); + vm.expectRevert(); + marketplaceFacet.setTicketFees(ticketId, _getFeeTypes(), _getFees()); + } + + // ====================================================================== + // WITHDRAW TO CONTRACT REVERTS + // ====================================================================== + + function test_withdrawTicketBalance_revertsContractNotAllowed() public { + (uint64 ticketId,,,) = _mintTicketETHRefundable(); + FullTicketData memory ftd = factoryFacet.ticketData(ticketId); + vm.warp(ftd.endTime + marketplaceFacet.getRefundPeriod()); + vm.expectRevert(ContractNotAllowed.selector); + marketplaceFacet.withdrawTicketBalance(ticketId, FeeType.ETH, hostIt); + } + + function test_withdrawHostItBalance_revertsContractNotAllowed() public { + _mintTicketETH(); + vm.expectRevert(ContractNotAllowed.selector); + marketplaceFacet.withdrawHostItBalance(FeeType.ETH, hostIt); + } + + function test_withdrawHostItBalance_revertsNonOwner() public { + _mintTicketETH(); + vm.prank(alice); + vm.expectRevert(); + marketplaceFacet.withdrawHostItBalance(FeeType.ETH, withdrawer); + } + + // ====================================================================== + // TICKET DOES NOT EXIST + // ====================================================================== + + function test_mintTicket_revertsTicketDoesNotExist() public { + vm.expectRevert(); + marketplaceFacet.mintTicket(999, FeeType.NONE, alice); + } + // ====================================================================== // MIXED: REFUNDABLE + NON-REFUNDABLE HOSTIT ACCUMULATION // ====================================================================== diff --git a/test/Ticket.t.sol b/test/Ticket.t.sol index 43a9c34..1d47820 100644 --- a/test/Ticket.t.sol +++ b/test/Ticket.t.sol @@ -1,10 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.30; +import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol"; +import {IERC721Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {ITicket} from "@ticket/interfaces/ITicket.sol"; import {Ticket} from "@ticket/libs/Ticket.sol"; import {Test} from "forge-std/Test.sol"; @@ -147,6 +151,109 @@ contract TicketTest is Test { assertNotEq(address(ticketClone), address(ticketClone2)); } + // ====================================================================== + // COVERAGE TESTS + // ====================================================================== + + function test_tokenURI_revertsNonExistentToken() public { + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, uint256(999))); + ticketClone.tokenURI(999); + } + + function test_supportsInterface_ERC721() public view { + assertTrue(ticketClone.supportsInterface(type(IERC721).interfaceId)); + } + + function test_supportsInterface_ERC721Enumerable() public view { + assertTrue(ticketClone.supportsInterface(type(IERC721Enumerable).interfaceId)); + } + + function test_supportsInterface_ERC2981() public view { + assertTrue(ticketClone.supportsInterface(type(IERC2981).interfaceId)); + } + + function test_supportsInterface_ERC165() public view { + assertTrue(ticketClone.supportsInterface(type(IERC165).interfaceId)); + } + + function test_supportsInterface_invalid() public view { + assertFalse(ticketClone.supportsInterface(0xffffffff)); + } + + function test_royaltyInfo() public { + uint256 tokenId = ticketClone.mint(alice); + (address receiver, uint256 royalty) = ticketClone.royaltyInfo(tokenId, 10_000); + assertEq(receiver, owner); + assertEq(royalty, 500); // 5% of 10_000 + } + + function test_defaultSymbol() public view { + assertEq(ticketClone.symbol(), "TICKET"); + } + + function test_initializeWithCustomSymbol() public { + Ticket clone2 = Ticket(address(ticketImpl).clone()); + clone2.initialize(owner, "Test", "CUSTOM", "ipfs://"); + assertEq(clone2.symbol(), "CUSTOM"); + } + + function test_revertDoubleInitialize() public { + vm.expectRevert(); + ticketClone.initialize(owner, "Again", "", "ipfs://"); + } + + function test_mintRevertsNonOwner() public { + vm.prank(alice); + vm.expectRevert(); + ticketClone.mint(alice); + } + + function test_pauseRevertsNonOwner() public { + vm.prank(alice); + vm.expectRevert(); + ticketClone.pause(); + } + + function test_unpauseRevertsNonOwner() public { + ticketClone.pause(); + vm.prank(alice); + vm.expectRevert(); + ticketClone.unpause(); + } + + function test_updateNameRevertsNonOwner() public { + vm.prank(alice); + vm.expectRevert(); + ticketClone.updateName("Hack"); + } + + function test_updateSymbolRevertsNonOwner() public { + vm.prank(alice); + vm.expectRevert(); + ticketClone.updateSymbol("HACK"); + } + + function test_updateURIRevertsNonOwner() public { + vm.prank(alice); + vm.expectRevert(); + ticketClone.updateURI("ipfs://hack"); + } + + function test_totalSupplyAndTokenByIndex() public { + ticketClone.mint(alice); + ticketClone.mint(bob); + assertEq(ticketClone.totalSupply(), 2); + assertEq(ticketClone.tokenByIndex(0), 1); + assertEq(ticketClone.tokenByIndex(1), 2); + } + + function test_tokenOfOwnerByIndex() public { + ticketClone.mint(alice); + ticketClone.mint(alice); + assertEq(ticketClone.tokenOfOwnerByIndex(alice, 0), 1); + assertEq(ticketClone.tokenOfOwnerByIndex(alice, 1), 2); + } + // ====================================================================== // FUZZ TESTS // ======================================================================