From 90841a90cdf93dbf142122ee3c2b1982f46e4d91 Mon Sep 17 00:00:00 2001 From: drquinn Date: Mon, 3 Jun 2024 17:11:49 -0500 Subject: [PATCH] public init --- .gitignore | 20 + LICENSE | 21 + README.md | 13 + contracts/Event.sol | 762 +++++++++++++++++++++++++ contracts/EventCollector.sol | 76 +++ contracts/MockToken.sol | 20 + contracts/Utils.sol | 20 + test/Event.ts | 1009 ++++++++++++++++++++++++++++++++++ test/EventCollector.ts | 188 +++++++ test/Event_DiscountMerkle.ts | 449 +++++++++++++++ test/Event_DiscountSig.ts | 286 ++++++++++ test/Event_PaidSingle.ts | 118 ++++ test/Event_Withdraw.ts | 294 ++++++++++ test/data.ts | 128 +++++ test/data_paidSingle.ts | 46 ++ 15 files changed, 3450 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 contracts/Event.sol create mode 100644 contracts/EventCollector.sol create mode 100644 contracts/MockToken.sol create mode 100644 contracts/Utils.sol create mode 100644 test/Event.ts create mode 100644 test/EventCollector.ts create mode 100644 test/Event_DiscountMerkle.ts create mode 100644 test/Event_DiscountSig.ts create mode 100644 test/Event_PaidSingle.ts create mode 100644 test/Event_Withdraw.ts create mode 100644 test/data.ts create mode 100644 test/data_paidSingle.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b28d78b --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types +.DS_Store + +# Hardhat files +cache +artifacts +cache-zk +artifacts-zk + +# Project files +hardhat.config.ts +scripts + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1417115 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Blocklive Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcc3a29 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Live Protocol + +This repository contains the smart contracts for the Live Protocol. + +This app uses Hardhat for Solidity smart contract development and deployment to EVM based networks. + +Steps to run: + +Copy & paste .env.example. Rename to .env + +Steps to test: + +At the root of the repo, run `npm test` diff --git a/contracts/Event.sol b/contracts/Event.sol new file mode 100644 index 0000000..ff9c050 --- /dev/null +++ b/contracts/Event.sol @@ -0,0 +1,762 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {ERC2981} from "@openzeppelin/contracts/token/common/ERC2981.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {Utils} from "./Utils.sol"; +import "operator-filter-registry/src/DefaultOperatorFilterer.sol"; + +/** + * @title EventTicketManager + * @notice This contract is used to manage the sale of tokens/tickets for an event. + * @custom:coauthor spatializes (Blocklive) + * @custom:coauthor daagcentral (Blocklive) + * @custom:coauthor aronvis (Blocklive) + */ + +contract Event is ERC1155, ERC2981, Utils, AccessControlEnumerable, DefaultOperatorFilterer { + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC1155, AccessControlEnumerable, ERC2981) returns (bool) { + return super.supportsInterface(interfaceId); + } + + using ECDSA for bytes32; + + enum DiscountType { + Merkle, + Signature + } + + struct TokenPrice { + bool exists; + uint256 value; + } + + /// @dev Register a discount with either a merkle root or signature + /// @dev DiscountBase contains the non-nested data used to describe the code for updates + struct DiscountBase { + string key; // Key for discount + string tokenType; // Token type key to apply discount to + uint256 value; // Basis points off token price + int256 maxUsesPerAddress; // Uses for the code per users (-1 inf) + int256 maxUsesTotal; // Uses for the code for all users (-1 inf) + DiscountType discountType; // Merkle or Signature based discount + bytes32 merkleRoot; // Merkle root containing addresses which are allowlisted + address signer; // Address used to sign discounts off chain + } + struct Discount { + bool exists; + DiscountBase base; + uint256 uses; + mapping(address => uint256) usesPerAddress; + } + + /// @dev Each type of token to be sold (ex: vip, premium) + /// @dev TokenTypeBase contains the non-nested data used to describe the token type for updates + struct TokenTypeBase { + string key; // Name of token type + string displayName; // Name of token type + int256 maxSupply; // Max number of token of this type (-1 inf) + bool active; // Token type can be purchased + bool locked; // Token is soulbound and cannot be transferred + bool gated; // Token cannot be purchased without a discount + } + + /// @dev Used to sync user address with role + struct RoleBase { + address userAddress; + bytes32 role; + } + + /// @dev Live data to track token type details on an active event. + struct TokenType { + bool exists; + TokenTypeBase base; + uint256 purchased; // Number of tokens purchased of this type + mapping(string => Discount) discount; // Mapping of discount key to Discount + mapping(string => TokenPrice) price; // Mapping of currency key to price + } + + /// @dev Each type of currency accepted for token purchases + struct CurrencyBase { + string tokenType; // Unique key for the token type + uint256 price; // Price for the token type + string currency; // Unique key for the erc20 token used for purchase + address currencyAddress; // Contract address for the erc20 token used for purchase + } + + /// @dev Split token sales between multiple addresses + struct Split { + bool exists; + address withdrawer; + uint256 percent; + uint256 base; + } + + /// @dev Each token sold + struct Token { + bool exists; + string tokenType; // Map to key of token type + address owner; // Address of token owner + bool locked; // Token is soulbound and cannot be transferred + bool valid; // Token is valid for event entry + } + + /// Permissions constants + bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE"); + bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); + + /// Contract version + string public version = "0.7.0"; + + /// Public name shown for collection title on marketplaces + string public name; + + /// Base URI for metadata reference + string private _uriBase; + + /// Event is active and tokens can be purchased + bool public active; + + // Limit of tokens per person per order total + uint256 public orderLimit = 5; + + // Limit of tokens per contract allowed + int256 public totalMaxSupply = -1; + + /// Mapping from token ID to its details + mapping(uint256 => Token) private _tokenRegistry; + + /// Mapping of token key to token type + mapping(string => TokenType) private _tokenTypeRegistry; + + Split[] public splitRegistry; + + /// Mapping from currency name to its ERC20 address + mapping(string => IERC20) public tokenCurrencies; + + // All currency keys for iteration + string[] private currencyKeys; + + /// Latest token id + uint256 public tokenIdCounter; + + event TokenPurchased(uint256 indexed tokenId, string indexed tokenType); + + constructor( + address creator, + string memory uribase, + string memory nameContract, + TokenTypeBase[] memory tokenTypeBase, + CurrencyBase[] memory currencyBase, + DiscountBase[] memory discountBase, + Split[] memory splits, + RoleBase[] memory roles + ) ERC1155(uribase) { + name = nameContract; + + /// @notice Assign creator to be owner + _grantRole(OWNER_ROLE, creator); + _grantRole(MANAGER_ROLE, creator); + + _uriBase = uribase; + active = true; + + /// @notice Initialze default split registry with 100% to creator + splitRegistry.push(Split(true, creator, 1, 1)); + + /// @notice Initialize royalties for ERC2981 + _setDefaultRoyalty(creator, 1000); + + /// @notice Initialize token types, currencies, discounts, splits + syncEventData(tokenTypeBase, currencyBase, discountBase, splits, roles); + } + + function setURIBase(string memory newuribase) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + _uriBase = newuribase; + } + + function setURI(string memory newuri) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + _setURI(newuri); + } + + function setDefaultRoyalty(address _receiver, uint96 _feeNumerator) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + _setDefaultRoyalty(_receiver, _feeNumerator); + } + + function uri(uint256 _id) public view override returns (string memory) { + return + string.concat(_uriBase, "/", toAsciiString(address(this)), "/", Strings.toString(_id)); + } + + function isNative(string memory currency) private pure returns (bool) { + return keccak256(abi.encodePacked(currency)) == keccak256(abi.encodePacked("native")); + } + + function buyToken( + string memory _tokenType, + uint256 amount, + address receiver, + string memory currency + ) public payable { + buyToken(_tokenType, amount, receiver, currency, address(0), "", new bytes32[](0), ""); + } + + /// @notice Purchase multiple tokens in a single txn + function buyToken( + string[] memory _tokenType, + uint256[] memory amount, + address[] memory receiver, + string[] memory currency, + address[] memory payer, + string[] memory discountCode, + bytes32[][] memory merkleProof, + bytes[] memory signature + ) public payable { + require(active, "Not active"); + + require( + _tokenType.length == amount.length && + _tokenType.length == receiver.length && + _tokenType.length == currency.length && + _tokenType.length == payer.length && + _tokenType.length == discountCode.length && + _tokenType.length == merkleProof.length && + _tokenType.length == signature.length, + "Arg length mismatch" + ); + + uint256 purchaseTotal = 0; + + for (uint256 i = 0; i < _tokenType.length; i++) { + uint256 price = buyToken( + _tokenType[i], + amount[i], + receiver[i], + currency[i], + payer[i], + discountCode[i], + merkleProof[i], + signature[i] + ); + if (isNative(currency[i])) { + purchaseTotal += price; + } + } + + require( + hasRole(OWNER_ROLE, msg.sender) || msg.value >= purchaseTotal, + "Not enough bal for batch" + ); + } + + /// @notice Purchase a token + function buyToken( + string memory _tokenType, // Unique key for type to be priced against + uint256 amount, // Amount to purchase multiple copies of a single token ID + address receiver, // Address to receive the token + string memory currency, // Currency to be used for purchase + address payer, // Address to pay for the token when ERC20 + string memory discountCode, // Discount code to be applied + bytes32[] memory merkleProof, // Merkle proof for discount + bytes memory signature // Signature for discount + ) public payable returns (uint256) { + address payerAddress = payer != address(0) ? payer : msg.sender; + + require(active, "Not active"); + + int256 maxSupply = _tokenTypeRegistry[_tokenType].base.maxSupply; + require( + maxSupply < 0 || + _tokenTypeRegistry[_tokenType].purchased + amount <= uint256(maxSupply), + "Max supply for token type" + ); + + require( + totalMaxSupply < 0 || tokenIdCounter + amount <= uint256(totalMaxSupply), + "Max supply for contract" + ); + + require(amount < orderLimit, "Exceeds limit"); + + require(_tokenTypeRegistry[_tokenType].base.active, "Token type is not active"); + + require( + isNative(currency) || address(tokenCurrencies[currency]) != address(0), + "Curr not registered" + ); + + require(_tokenTypeRegistry[_tokenType].price[currency].exists, "Type not registered"); + + TokenPrice memory price = _tokenTypeRegistry[_tokenType].price[currency]; + Discount storage discount = _tokenTypeRegistry[_tokenType].discount[discountCode]; + + if ( + _tokenTypeRegistry[_tokenType].base.gated && + !discount.exists && + !hasRole(OWNER_ROLE, msg.sender) + ) { + revert("Token type is gated"); + } + + if (discount.exists) { + if (discount.base.discountType == DiscountType.Signature) { + require( + _verifySignature(receiver, signature, discount.base.signer), + "Not on signature allow list" + ); + } else if (discount.base.discountType == DiscountType.Merkle) { + require( + _verifyAddress(merkleProof, discount.base.merkleRoot, receiver), + "Not on merkle allow list" + ); + } else { + revert("Invalid discount type"); + } + + price.value = price.value - ((price.value * discount.base.value) / 10000); + + int256 maxUsesTotal = discount.base.maxUsesTotal; + int256 maxUsesPerAddress = discount.base.maxUsesPerAddress; + require( + maxUsesTotal < 0 || discount.uses + amount <= uint256(maxUsesTotal), + "Max uses total reached" + ); + require( + maxUsesPerAddress < 0 || + discount.usesPerAddress[receiver] + amount <= uint256(maxUsesPerAddress), + "Max uses reached for address" + ); + + discount.uses += 1; + discount.usesPerAddress[receiver] += 1; + } + + if (isNative(currency)) { + require( + hasRole(OWNER_ROLE, msg.sender) || msg.value >= price.value * amount, + "Not enough bal" + ); + } else { + if (!hasRole(OWNER_ROLE, msg.sender)) { + require( + tokenCurrencies[currency].balanceOf(payerAddress) >= price.value * amount, + "Not enough bal" + ); + + tokenCurrencies[currency].transferFrom( + payerAddress, + address(this), + price.value * amount + ); + } + } + + uint256 tokenId = tokenIdCounter; + tokenIdCounter += 1; + + _tokenTypeRegistry[_tokenType].purchased += 1; + + _tokenRegistry[tokenId].tokenType = _tokenType; + + emit TokenPurchased(tokenId, _tokenType); + _mint(receiver, tokenId, 1, ""); + + return price.value; + } + + /// @notice Register token types + /// @param tokenTypeBase Array of token type base structs + function registerTokenType(TokenTypeBase[] memory tokenTypeBase) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + for (uint256 i = 0; i < tokenTypeBase.length; i++) { + TokenType storage _tokenType = _tokenTypeRegistry[tokenTypeBase[i].key]; + _tokenType.base = tokenTypeBase[i]; + _tokenType.exists = true; + } + } + + /// @notice Register a currency to be accepted for a token type and price + /// @param currencyBase Array of currency base structs + function registerCurrency(CurrencyBase[] memory currencyBase) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + for (uint256 i = 0; i < currencyBase.length; i++) { + TokenType storage _tokenType = _tokenTypeRegistry[currencyBase[i].tokenType]; + string memory ckey = currencyBase[i].currency; + address caddr = currencyBase[i].currencyAddress; + require(_tokenType.exists, "Token key not registered"); + + _tokenType.price[ckey] = TokenPrice(true, currencyBase[i].price); + + if (caddr != address(0) && tokenCurrencies[ckey] != IERC20(caddr)) { + tokenCurrencies[ckey] = IERC20(caddr); + currencyKeys.push(ckey); + } + } + } + + function _verifyAddress( + bytes32[] memory merkleProof, + bytes32 merkleRoot, + address receiver + ) private pure returns (bool) { + bytes32 leafAddress = keccak256(abi.encodePacked(receiver)); + return MerkleProof.verify(merkleProof, merkleRoot, leafAddress); + } + + function _verifySignature( + address allowedAddress, + bytes memory signature, + address signer + ) internal pure returns (bool _isValid) { + bytes32 digest = keccak256( + abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + keccak256(abi.encode(allowedAddress)) + ) + ); + + return signer == digest.recover(signature); + } + + /// @notice Register a discount for an event + /// @param discountBase Base input discount data + function registerDiscount(DiscountBase[] memory discountBase) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + for (uint256 i = 0; i < discountBase.length; i++) { + Discount storage discount = _tokenTypeRegistry[discountBase[i].tokenType].discount[ + discountBase[i].key + ]; + discount.base = discountBase[i]; + discount.exists = true; + } + } + + function setRole(address user, bytes32 role) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + _grantRole(role, user); + } + + function removeRole(address user, bytes32 role) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + _revokeRole(role, user); + } + + function setActive(bool _active) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + active = _active; + } + + function setLimit(uint256 limit) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + + orderLimit = limit; + } + + function setTotalMaxSupply(int256 _totalMaxSupply) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + + totalMaxSupply = _totalMaxSupply; + } + + /// @notice Token Type Registry helpers + function tokenActive(string memory _tokenType) public view returns (bool) { + return _tokenTypeRegistry[_tokenType].base.active; + } + + function tokenAmounts(string memory _tokenType) public view returns (int256) { + return _tokenTypeRegistry[_tokenType].base.maxSupply; + } + + function tokensPurchased(string memory _tokenType) public view returns (uint256) { + return _tokenTypeRegistry[_tokenType].purchased; + } + + /// @notice Token Registry helpers + function owned(uint256 tokenId) public view returns (address) { + return _tokenRegistry[tokenId].owner; + } + + function tokenType(uint256 tokenId) public view returns (string memory) { + return _tokenRegistry[tokenId].tokenType; + } + + function tokenLocked(uint256 tokenId) public view returns (bool) { + return _tokenRegistry[tokenId].locked; + } + + function setTokenLock(uint256 tokenId, bool locked) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + _tokenRegistry[tokenId].locked = locked; + } + + /// @notice Register roles + function registerRoles(RoleBase[] memory roles) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + for (uint256 i = 0; i < roles.length; i++) { + _grantRole(roles[i].role, roles[i].userAddress); + } + } + + /// @notice Address splits for withdraws from the contract + function getSplits() external view returns (Split[] memory) { + return splitRegistry; + } + + function registerSplits(Split[] memory splits) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + if (splits.length < 1) { + return; + } + + // Always register fresh splits + delete splitRegistry; + + uint256 base = splits[0].base; + uint256 total = 0; + for (uint256 i = 0; i < splits.length; i++) { + total += splits[i].percent; + splitRegistry.push(splits[i]); + require(splits[i].base == base, "Splits must have same base"); + } + require(total / base == 1, "Splits must add up to 100%"); + } + + function sweep(string memory currency, Split[] memory splits) internal { + if (keccak256(abi.encodePacked(currency)) == keccak256(abi.encodePacked("native"))) { + uint256 _balance = address(this).balance; + for (uint i = 0; i < splits.length; i++) { + uint256 splitBalance = (_balance * splits[i].percent) / splits[i].base; + payable(splits[i].withdrawer).transfer(splitBalance); + } + } else { + IERC20 token = tokenCurrencies[currency]; + uint256 _balance = token.balanceOf(address(this)); + for (uint i = 0; i < splits.length; i++) { + uint256 splitBalance = (_balance * splits[i].percent) / splits[i].base; + token.transfer(splits[i].withdrawer, splitBalance); + } + } + } + + function sweepSplit(string memory currency) public { + require(hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), "No access"); + sweep(currency, splitRegistry); + } + + /// @notice Full sweep for the manager as backup if the split registry is corrupted + function sweepAll(address sweeper) public { + require(hasRole(OWNER_ROLE, msg.sender), "Access Denied"); + + Split[] memory splits = new Split[](1); + splits[0] = Split(true, sweeper, 1, 1); + + // Sweep all erc20 tokens + for (uint256 i = 0; i < currencyKeys.length; i++) { + string memory ckey = currencyKeys[i]; + sweep(ckey, splits); + } + + // Sweep native currency + sweep("native", splits); + } + + /// @notice Sync all event data + /// @notice (token types, pricing, discounts) + /// @dev each will be noop if empty array is passed + function syncEventData( + TokenTypeBase[] memory tokenTypeBase, + CurrencyBase[] memory currencyBase, + DiscountBase[] memory discountBase, + Split[] memory splits, + RoleBase[] memory roles + ) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + registerTokenType(tokenTypeBase); + registerCurrency(currencyBase); + registerDiscount(discountBase); + registerSplits(splits); + registerRoles(roles); + } + + function rescueToken( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + _safeTransferFrom(from, to, id, amount, data); + } + + /// @notice Call for offchain updates to metadata json + function metadataUpdated(uint256 tokenId) public { + require( + hasRole(OWNER_ROLE, msg.sender) || hasRole(MANAGER_ROLE, msg.sender), + "Access Denied" + ); + emit URI(uri(tokenId), tokenId); + } + + function contractBalances() public view returns (uint256) { + uint256 balances = 0; + for (uint256 i = 0; i < currencyKeys.length; i++) { + balances += tokenCurrencies[currencyKeys[i]].balanceOf(address(this)); + } + balances += address(this).balance; + return balances; + } + + function renounceAccessControl() public { + require(hasRole(OWNER_ROLE, msg.sender), "Access Denied"); + require(contractBalances() == 0, "Bal is not zero"); + + bytes32[2] memory roles = [OWNER_ROLE, MANAGER_ROLE]; + + for (uint256 r = 0; r < roles.length; r++) { + for (uint256 m = 0; m < getRoleMemberCount(roles[r]); m++) { + revokeRole(roles[r], getRoleMember(roles[r], m)); + } + } + } + + /// Overrides + /// @notice Keep the list of owners up to date on all transfers, mints, burns + function _update( + address from, + address to, + uint256[] memory ids, + uint256[] memory values + ) internal virtual override { + // To run **before** the transfer + + super._update(from, to, ids, values); + + // To run **after** the transfer + for (uint256 i = 0; i < ids.length; ++i) { + uint256 id = ids[i]; + _tokenRegistry[id].owner = to; + } + } + + /// Ensure token and token type are both unlocked + function checkOnlyUnlocked(uint256 tokenId) public view { + /// Check token is not locked + require(_tokenRegistry[tokenId].locked == false, "Token is locked"); + + /// Check token type is not locked + require( + _tokenTypeRegistry[_tokenRegistry[tokenId].tokenType].base.locked == false, + "Token type is locked" + ); + } + + /// @notice modifier to block batch transfers when token is locked + modifier onlyUnlockedBatch(uint256[] memory tokenId) { + /// Check all tokens, only allow if all are unlocked + for (uint256 i = 0; i < tokenId.length; ++i) { + checkOnlyUnlocked(tokenId[i]); + } + _; + } + + /// @notice modifier to block transfers when token is locked + modifier onlyUnlocked(uint256 tokenId) { + /// Check all tokens, only allow if all are unlocked + checkOnlyUnlocked(tokenId); + _; + } + + /// @notice Filter registry from OpenSea. + /// @dev See {IERC1155-setApprovalForAll}. + /// @dev In this example the added modifier ensures that the operator is allowed by the OperatorFilterRegistry. + function setApprovalForAll( + address operator, + bool approved + ) public override onlyAllowedOperatorApproval(operator) { + super.setApprovalForAll(operator, approved); + } + + /// @dev See {IERC1155-safeTransferFrom}. + /// @dev In this example the added modifier ensures that the operator is allowed by the OperatorFilterRegistry. + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + uint256 amount, + bytes memory data + ) public override onlyAllowedOperator(from) onlyUnlocked(tokenId) { + super.safeTransferFrom(from, to, tokenId, amount, data); + } + + /// @dev See {IERC1155-safeBatchTransferFrom}. + /// @dev In this example the added modifier ensures that the operator is allowed by the OperatorFilterRegistry. + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual override onlyAllowedOperator(from) onlyUnlockedBatch(ids) { + super.safeBatchTransferFrom(from, to, ids, amounts, data); + } +} diff --git a/contracts/EventCollector.sol b/contracts/EventCollector.sol new file mode 100644 index 0000000..5cdf526 --- /dev/null +++ b/contracts/EventCollector.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title EventCollector + * @notice This contract is used to bridge funds between networks + */ + +// Add owners + +contract EventCollector is AccessControl { + bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE"); + + // Mapping from currency names to contract addresses + mapping(string => address) public currencyAddresses; + + constructor(string[] memory currencyName, address[] memory currencyAddress) { + _grantRole(OWNER_ROLE, msg.sender); + + // Initialize known currencies + currencyAddresses["native"] = address(0); + for (uint i = 0; i < currencyName.length; i++) { + currencyAddresses[currencyName[i]] = currencyAddress[i]; + } + } + + /// @notice Purchase a token + /// @param _tokenType Unique key for token type + // TODO: map buys to events + function buyToken( + string memory _tokenType, + uint256 amount, + address receiver, + string memory currency, + address payer, + string memory discountCode, + bytes32[] memory merkleProof, + bytes memory signature + ) public payable { + address currencyAddress = currencyAddresses[currency]; + + // Need the min payment checks native / erc20 + if (currencyAddress == address(0)) { + require(msg.value >= amount, "ETH sent does not match or exceed amount"); + } else { + IERC20 token = IERC20(currencyAddress); + require(token.transferFrom(msg.sender, address(this), amount), "Transfer failed"); + } + } + + function sweepFunds( + address payable ethTo, + address[] memory tokens, + address[] memory tokenRecipients + ) public onlyRole(OWNER_ROLE) { + // Sweep Ether + uint256 ethBalance = address(this).balance; + if (ethBalance > 0) { + (bool success, ) = ethTo.call{value: ethBalance}(""); + require(success, "Failed to send Ether"); + } + + // Sweep Tokens + for (uint i = 0; i < tokens.length; i++) { + IERC20 token = IERC20(tokens[i]); + uint256 tokenBalance = token.balanceOf(address(this)); + if (tokenBalance > 0) { + require(token.transfer(tokenRecipients[i], tokenBalance), "Failed to send tokens"); + } + } + } +} diff --git a/contracts/MockToken.sol b/contracts/MockToken.sol new file mode 100644 index 0000000..f08cff4 --- /dev/null +++ b/contracts/MockToken.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract MockToken is ERC20, Ownable { + constructor( + string memory name, + string memory symbol, + uint256 initialSupply, + address initialOwner + ) ERC20(name, symbol) Ownable(initialOwner) { + _mint(msg.sender, initialSupply); + } + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} diff --git a/contracts/Utils.sol b/contracts/Utils.sol new file mode 100644 index 0000000..5d02f16 --- /dev/null +++ b/contracts/Utils.sol @@ -0,0 +1,20 @@ +pragma solidity ^0.8.4; + +contract Utils { + function toAsciiString(address x) internal pure returns (string memory) { + bytes memory s = new bytes(40); + for (uint i = 0; i < 20; i++) { + bytes1 b = bytes1(uint8(uint(uint160(x)) / (2**(8*(19 - i))))); + bytes1 hi = bytes1(uint8(b) / 16); + bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi)); + s[2*i] = char(hi); + s[2*i+1] = char(lo); + } + return string(s); + } + + function char(bytes1 b) internal pure returns (bytes1 c) { + if (uint8(b) < 10) return bytes1(uint8(b) + 0x30); + else return bytes1(uint8(b) + 0x57); + } +} \ No newline at end of file diff --git a/test/Event.ts b/test/Event.ts new file mode 100644 index 0000000..602de14 --- /dev/null +++ b/test/Event.ts @@ -0,0 +1,1009 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { eventData, priceData } from "./data"; +const hre = require("hardhat"); + +const { AddressZero } = ethers.constants; + +describe("Event", function () { + // We define a fixture to reuse the same setup in every test. + // We use loadFixture to run this setup once, snapshot that state, + // and reset Hardhat Network to that snapshot in every test. + async function deployFixture() { + // Contracts are deployed using the first signer/account by default + const [owner, otherAccount] = await ethers.getSigners(); + + const MockTokenFactory = await ethers.getContractFactory("MockToken"); + const mockToken = await MockTokenFactory.deploy( + "USD COIN", + "USDC", + 0, + owner.address + ); + + const Event = await ethers.getContractFactory("Event"); + + const event = await Event.deploy( + owner.address, + eventData.uri, + eventData.details._name, + [], + [], + [], + [], + [] + ); + + priceData.priceBase[2].currencyAddress = mockToken.address; + priceData.priceBase[4].currencyAddress = mockToken.address; + await event.registerTokenType(eventData.ticketBase); + await event.registerCurrency(priceData.priceBase); + + return { event, owner, otherAccount, mockToken }; + } + + describe("Deployment", function () { + it("Should set the right owner", async function () { + const { event, owner } = await loadFixture(deployFixture); + + const OWNER_ROLE = await event.OWNER_ROLE(); + expect(await event.hasRole(OWNER_ROLE, owner.address)).to.equal(true); + }); + + it("Should set up ticket types", async function () { + const { event } = await loadFixture(deployFixture); + + // Amount of tickets available for each type is set. + expect((await event.tokenAmounts("free")).toNumber()).to.equal( + eventData.amounts[0] + ); + expect((await event.tokenAmounts("vip")).toNumber()).to.equal( + eventData.amounts[1] + ); + + expect((await event.tokensPurchased("free")).toNumber()).to.equal(0); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(0); + + // Latest tokens are set up with proper ids + expect(await event.tokenIdCounter()).to.equal(0); + expect(await event.tokenIdCounter()).to.equal(0); + }); + + it("Should not allow us to register a currency until we register a ticket", async function () { + const { event, mockToken } = await loadFixture(deployFixture); + + const newCurrency = { + tokenType: "special", + price: 0, + currency: "sol", + currencyAddress: mockToken.address, + }; + await expect( + // event.registerCurrency(["special"], [0], ["sol"], [mockToken.address]) + event.registerCurrency([newCurrency]) + ).to.be.revertedWith("Token key not registered"); + }); + + it("Should not allow us to buy when not active", async function () { + const { event, owner } = await loadFixture(deployFixture); + + // Set inactive, fail to buy ticket + await event.setActive(false); + await expect( + event["buyToken(string,uint256,address,string)"]( + "free", + 1, + owner.address, + "native" + ) + ).to.be.revertedWith("Not active"); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(0); + + // Set active, succeed to buy ticket + await event.setActive(true); + await event["buyToken(string,uint256,address,string)"]( + "free", + 1, + owner.address, + "native" + ); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(1); + }); + + it("Should let us buy tickets", async function () { + const { event, owner, otherAccount } = await loadFixture(deployFixture); + + // Buy a ticket, count increases + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 1, + otherAccount.address, + "native" + ); + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(1); + + // Buy 1 more + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 1, + otherAccount.address, + "native" + ); + expect(await event.tokenIdCounter()).to.equal(2); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(2); + + // Buy 1 more from the second ticket type without enough eth + await expect( + event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "vip", + 1, + otherAccount.address, + "native", + { + value: ethers.utils.parseEther("0.99"), + } + ) + ).to.be.revertedWith("Not enough bal"); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(0); + + // Buy 1 more from the second ticket type with enough eth + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "vip", + 1, + otherAccount.address, + "native", + { + value: ethers.utils.parseEther("1"), + } + ); + expect(await event.tokenIdCounter()).to.equal(3); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(1); + + // Check to see if the contract has the eth sent + expect(await event.provider.getBalance(event.address)).to.equal( + ethers.utils.parseEther("1") + ); + + // Buy more from the second ticket type with enough eth + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "vip", + 1, + otherAccount.address, + "native", + { + value: ethers.utils.parseEther("1"), + } + ); + + expect(await event.tokenIdCounter()).to.equal(4); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(2); + + // Checked the owned + expect(await event.owned(0)).to.be.equal(otherAccount.address); + expect(await event.owned(3)).to.be.equal(otherAccount.address); + + // Ticket type should be visible for tokens + expect(await event.tokenType(0)).to.be.equal("free"); + expect(await event.tokenType(3)).to.be.equal("vip"); + }); + + it("Should let us buy multiple native tickets", async function () { + const { event, owner, otherAccount } = await loadFixture(deployFixture); + + // Buy 1 ticket to start + const tokenTypes = ["free"]; // string[] + const amounts = [1]; // uint256[] + const receivers = [otherAccount.address]; // address[] + const currencies = ["native"]; // string[] + const payers = [otherAccount.address]; // address[] + const discountCodes = [""]; // string[] + const merkleProofs = [[]]; // bytes32[][] + const signatures = [[]]; // bytes[] + + const funcSignature = + "buyToken(string[],uint256[],address[],string[],address[],string[],bytes32[][],bytes[])"; + const tx = await event + .connect(otherAccount) + .functions[funcSignature]( + tokenTypes, + amounts, + receivers, + currencies, + payers, + discountCodes, + merkleProofs, + signatures + ); + await tx.wait(); + + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(1); + + // Buy 5 more tickets of different types + const buyTwoTx = await event + .connect(otherAccount) + .functions[ + "buyToken(string[],uint256[],address[],string[],address[],string[],bytes32[][],bytes[])" + ]( + ["free", "free", "vip", "vip", "vip"], + [1, 1, 1, 1, 1], + [ + otherAccount.address, + otherAccount.address, + otherAccount.address, + otherAccount.address, + otherAccount.address, + ], + ["native", "native", "native", "native", "native"], + [ + otherAccount.address, + otherAccount.address, + otherAccount.address, + otherAccount.address, + otherAccount.address, + ], + ["", "", "", "", ""], + [[], [], [], [], []], + [[], [], [], [], []], + { + value: ethers.utils.parseEther("3"), + } + ); + const buyTwoTxRecipt = await buyTwoTx.wait(); + const mintedTokens: number[] = []; + buyTwoTxRecipt?.events?.forEach((tsEvent) => { + if (tsEvent.event === "TransferSingle") { + mintedTokens.push(tsEvent.args?.id); + } + }); + + // Expect 5 tokens to be minted with ids 1-5 + expect(mintedTokens?.length).to.equal(5); + expect(mintedTokens[0]).to.equal(1); + expect(mintedTokens[1]).to.equal(2); + expect(mintedTokens[2]).to.equal(3); + expect(mintedTokens[3]).to.equal(4); + expect(mintedTokens[4]).to.equal(5); + + // Expect token types in order of minting array + expect(await event.tokenType(1)).to.be.equal("free"); + expect(await event.tokenType(2)).to.be.equal("free"); + expect(await event.tokenType(3)).to.be.equal("vip"); + expect(await event.tokenType(4)).to.be.equal("vip"); + expect(await event.tokenType(5)).to.be.equal("vip"); + + expect(await event.tokenIdCounter()).to.equal(6); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(3); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(3); + + expect(await event.provider.getBalance(event.address)).to.equal( + ethers.utils.parseEther("3") + ); + + // Buy 3 more VIP, should fail with only enough price for 2 + await expect( + event + .connect(otherAccount) + .functions[ + "buyToken(string[],uint256[],address[],string[],address[],string[],bytes32[][],bytes[])" + ]( + ["vip", "vip", "vip"], + [1, 1, 1], + [otherAccount.address, otherAccount.address, otherAccount.address], + ["native", "native", "native"], + [otherAccount.address, otherAccount.address, otherAccount.address], + ["", "", ""], + [[], [], []], + [[], [], []], + { + value: ethers.utils.parseEther("2"), + } + ) + ).to.be.revertedWith("Not enough bal for batch"); + + // // Buy 50 tickets + const tix50 = Array(50).fill(null); + await event + .connect(otherAccount) + .functions[ + "buyToken(string[],uint256[],address[],string[],address[],string[],bytes32[][],bytes[])" + ]( + tix50.map(() => "free"), + tix50.map(() => 1), + tix50.map(() => otherAccount.address), + tix50.map(() => "native"), + tix50.map(() => otherAccount.address), + tix50.map(() => ""), + tix50.map(() => []), + tix50.map(() => []) + ); + + expect(await event.tokenIdCounter()).to.equal(56); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(53); + }); + + it("Should let us buy multiple usdc tickets", async function () { + const { event, owner, otherAccount, mockToken } = await loadFixture( + deployFixture + ); + + // Buy 3 Premium tickets with USDC + // Give buyer exact # of tokens needed for ticket price. + await mockToken.mint(otherAccount.address, 6000000); + // Approve exact # of tokens needed for ticket price. + await mockToken.connect(otherAccount).approve(event.address, 6000000); + await event + .connect(otherAccount) + .functions[ + "buyToken(string[],uint256[],address[],string[],address[],string[],bytes32[][],bytes[])" + ]( + ["premium", "premium", "premium"], + [1, 1, 1], + [otherAccount.address, otherAccount.address, otherAccount.address], + ["usdc", "usdc", "usdc"], + [otherAccount.address, otherAccount.address, otherAccount.address], + ["", "", ""], + [[], [], []], + [[], [], []] + ); + + expect(await event.tokenIdCounter()).to.equal(3); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(3); + + // Give buyer only enough # of tokens needed for 2 /3 tickets. + await mockToken.mint(otherAccount.address, 4000000); + await mockToken.connect(otherAccount).approve(event.address, 4000000); + await expect( + event + .connect(otherAccount) + .functions[ + "buyToken(string[],uint256[],address[],string[],address[],string[],bytes32[][],bytes[])" + ]( + ["premium", "premium", "premium"], + [1, 1, 1], + [otherAccount.address, otherAccount.address, otherAccount.address], + ["usdc", "usdc", "usdc"], + [otherAccount.address, otherAccount.address, otherAccount.address], + ["", "", ""], + [[], [], []], + [[], [], []] + ) + ).to.be.revertedWith("Not enough bal"); + + expect(await event.tokenIdCounter()).to.equal(3); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(3); + }); + + it("Should let us change ticket price", async function () { + const { event, owner, otherAccount } = await loadFixture(deployFixture); + + // Buy 1 ticket with enough ETH + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 1, + otherAccount.address, + "native", + { + value: ethers.utils.parseEther("1"), + } + ); + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(1); + + const newCurrency = { + tokenType: "free", + price: ethers.utils.parseEther("2"), + currency: "native", + currencyAddress: ethers.constants.AddressZero, + }; + await event.registerCurrency([newCurrency]); + + // Buy another ticket with same 1 ETH, should fail + await expect( + event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 1, + otherAccount.address, + "native", + { + value: ethers.utils.parseEther("1"), + } + ) + ).to.be.revertedWith("Not enough bal"); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(1); + + // Buy another ticket with enough ETH + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 1, + otherAccount.address, + "native", + { + value: ethers.utils.parseEther("2"), + } + ); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(2); + }); + + it("Should let an owner buy without any eth", async function () { + const { event, owner, mockToken, otherAccount } = await loadFixture( + deployFixture + ); + + await event["buyToken(string,uint256,address,string)"]( + "vip", + 1, + otherAccount.address, + "native" + ); + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(1); + }); + + it("Should let an owner buy without any USDC", async function () { + const { event, owner, mockToken } = await loadFixture(deployFixture); + + // NO tokens needed. + // No approval on token needed. + + // Buy 1 ticket of type 2 (usdc) with enough usdc + await event["buyToken(string,uint256,address,string)"]( + "premium", + 1, + owner.address, + "usdc" + ); + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(1); + + expect(await mockToken.balanceOf(owner.address)).to.equal(0); + }); + + it("Should let a user buy with USDC", async function () { + const { event, mockToken, otherAccount } = await loadFixture( + deployFixture + ); + + // Give buyer exact # of tokens needed for ticket price. + await mockToken.mint(otherAccount.address, 2000000); + + // Approve exact # of tokens needed for ticket price. + await mockToken.connect(otherAccount).approve(event.address, 2000000); + + // Buy 1 ticket of type 2 (usdc) with enough usdc + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "premium", + 1, + otherAccount.address, + "usdc" + ); + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(1); + + expect(await event.contractBalances()).to.equal(2000000); + + // Buy another ticket of type 2 (usdc), not enough approved, should fail. + await expect( + event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "premium", + 1, + otherAccount.address, + "usdc" + ) + ).to.be.revertedWith("Not enough bal"); + + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(1); + }); + + it("Should prevent buying when not enough usdc has been allowed for amount", async function () { + const { event, owner, mockToken, otherAccount } = await loadFixture( + deployFixture + ); + + // Give owner exact # of tokens needed for 1 ticket price. + await mockToken.mint(otherAccount.address, 2000000); + + // Approve exact # of tokens needed for ticket price. + await mockToken.connect(otherAccount).approve(event.address, 2000000); + + // Buy 2 ticket of type 2 (usdc) with enough usdc + await expect( + event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "premium", + 2, + otherAccount.address, + "usdc" + ) + ).to.be.reverted; + + expect(await event.tokenIdCounter()).to.equal(0); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(0); + }); + + it("Should prevent buying when currency isnt registered for a ticket type", async function () { + const { event, owner, mockToken, otherAccount } = await loadFixture( + deployFixture + ); + + // Give owner exact # of tokens needed for ticket price. + await mockToken.mint(otherAccount.address, 2000000); + + // Approve exact # of tokens needed for ticket price. + await mockToken.connect(otherAccount).approve(event.address, 2000000); + + await expect( + event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "vip", + 1, + otherAccount.address, + "usdc" + ) + ).to.be.revertedWith("Type not registered"); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(0); + }); + + it("Should prevent buying beyond the order limit", async function () { + const { event, owner, otherAccount } = await loadFixture(deployFixture); + + await expect( + event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 6, + otherAccount.address, + "native" + ) + ).to.be.revertedWith("Exceeds limit"); + + expect((await event.tokensPurchased("free")).toNumber()).to.equal(0); + + // Set limit lower, fail to buy ticket + await event.setLimit(2); + + await expect( + event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 3, + otherAccount.address, + "native" + ) + ).to.be.revertedWith("Exceeds limit"); + }); + + it("Should prevent buying when ticket is disabled", async function () { + const { event, owner, otherAccount } = await loadFixture(deployFixture); + + // Set inactive, fail to buy ticket + // await event.setTokenActive("free", false); + const ticketInactive = { + key: "free", + displayName: "free", + maxSupply: 100, + active: false, + locked: false, + }; + await event.registerTokenType([ticketInactive]); + + await expect( + event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 1, + otherAccount.address, + "native" + ) + ).to.be.revertedWith("Token type is not active"); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(0); + + const ticketActive = { + key: "free", + displayName: "free", + maxSupply: 100, + active: true, + locked: false, + }; + await event.registerTokenType([ticketActive]); + + // Set active, succeed to buy ticket + // await event.setTokenActive("free", true); + + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 1, + otherAccount.address, + "native" + ); + expect((await event.tokensPurchased("free")).toNumber()).to.equal(1); + }); + + it("Should allow us to set and revoke a role", async function () { + const { event, owner, otherAccount } = await loadFixture(deployFixture); + + const OWNER_ROLE = await event.OWNER_ROLE(); + // Non owner cannot set active. + await expect(event.connect(otherAccount).setActive(false)).to.be.reverted; + expect(await event.active()).to.be.true; + + // Make owner, now can set active. + await event.setRole(otherAccount.address, OWNER_ROLE); + await event.connect(otherAccount).setActive(false); + expect(await event.active()).to.be.false; + }); + + // it("Should allow us to update supply", async function () { + // const { event, owner, otherAccount } = await loadFixture(deployFixture); + + // // Open up limit + // await event.setLimit(10000); + + // // Fail to buy over the max supply (100) + // await expect( + // event.connect(otherAccount)["buyToken(string,uint256,address,string)"]( + // "free", + // // 101, + // 1, + // otherAccount.address, + // "native" + // ) + // ).to.be.reverted; + // expect((await event.tokensPurchased("free")).toNumber()).to.equal(0); + + // // Successfully buy all of supply + // await event + // .connect(otherAccount) + // ["buyToken(string,uint256,address,string)"]( + // "free", + // 1, + // otherAccount.address, + // "native" + // ); + // expect((await event.tokensPurchased("free")).toNumber()).to.equal(100); + + // // Fail to buy 1 over the max supply (100) + // await expect( + // event + // .connect(otherAccount) + // ["buyToken(string,uint256,address,string)"]( + // "free", + // 1, + // otherAccount.address, + // "native" + // ) + // ).to.be.reverted; + // expect((await event.tokensPurchased("free")).toNumber()).to.equal(100); + + // const tokenSupplyUpdate = { + // key: "free", + // displayName: "free", + // maxSupply: 110, + // active: true, + // locked: false, + // }; + // await event.registerTokenType([tokenSupplyUpdate]); + + // // await event.setTicketSupply("free", 110); + + // // Succeed to buy 1 with new max supply (110) + // await event + // .connect(otherAccount) + // ["buyToken(string,uint256,address,string)"]( + // "free", + // 1, + // otherAccount.address, + // "native" + // ); + // expect((await event.tokensPurchased("free")).toNumber()).to.equal(101); + + // // Fail to buy over the new max supply (110) + // await expect( + // event + // .connect(otherAccount) + // ["buyToken(string,uint256,address,string)"]( + // "free", + // 10, + // otherAccount.address, + // "native" + // ) + // ).to.be.reverted; + // expect((await event.tokensPurchased("free")).toNumber()).to.equal(101); + // }); + + // it("Should allow us to update supply", async function () { + // const { event, otherAccount } = await loadFixture(deployFixture); + + // // Open up limit on all tickets + // await event.setLimit(10000); + + // // Buy 1 ticket successfully + // await event + // .connect(otherAccount) + // ["buyToken(string,uint256,address,string)"]( + // "free", + // 1, + // otherAccount.address, + // "native" + // ); + // expect((await event.tokensPurchased("free")).toNumber()).to.equal(1); + + // // Set 3 ticket total supply + // await event.setTotalMaxSupply(20); + + // // Fail to buy over the max supply (20) + // await expect( + // event + // .connect(otherAccount) + // ["buyToken(string,uint256,address,string)"]( + // "free", + // 20, + // otherAccount.address, + // "native" + // ) + // ).to.be.reverted; + // expect((await event.tokensPurchased("free")).toNumber()).to.equal(1); + + // // Successfully buy within supply range + // await event + // .connect(otherAccount) + // ["buyToken(string,uint256,address,string)"]( + // "free", + // 19, + // otherAccount.address, + // "native" + // ); + // expect((await event.tokensPurchased("free")).toNumber()).to.equal(20); + // }); + + it("Should prevent transfer by account not holding the nft, but allow owner revoke transfer", async function () { + const { event, owner, otherAccount } = await loadFixture(deployFixture); + + // Get 2 tickets + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 1, + otherAccount.address, + "native" + ); + + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 1, + otherAccount.address, + "native" + ); + + expect((await event.tokensPurchased("free")).toNumber()).to.equal(2); + let balToken0 = await event.balanceOf(otherAccount.address, 0); + let balToken1 = await event.balanceOf(otherAccount.address, 1); + expect(balToken0.toNumber()).to.equal(1); + expect(balToken1.toNumber()).to.equal(1); + + // Transfer 1 away + await event + .connect(otherAccount) + .safeTransferFrom(otherAccount.address, owner.address, 0, 1, "0x"); + + balToken0 = await event.balanceOf(otherAccount.address, 0); + balToken1 = await event.balanceOf(otherAccount.address, 1); + expect(balToken0.toNumber()).to.equal(0); + expect(balToken1.toNumber()).to.equal(1); + + // Try to transfer as owner, fail + await expect( + event.safeTransferFrom(otherAccount.address, owner.address, 1, 1, "0x") + ).to.be.reverted; + + // Transfer as owner + await event.rescueToken(otherAccount.address, owner.address, 1, 1, "0x"); + balToken0 = await event.balanceOf(otherAccount.address, 0); + balToken1 = await event.balanceOf(otherAccount.address, 1); + expect(balToken0.toNumber()).to.equal(0); + expect(balToken1.toNumber()).to.equal(0); + + const balTokenOwner0 = await event.balanceOf(owner.address, 0); + const balTokenOwner1 = await event.balanceOf(owner.address, 1); + expect(balTokenOwner0.toNumber()).to.equal(1); + expect(balTokenOwner1.toNumber()).to.equal(1); + }); + + it("Should emit URI event on update metadata", async function () { + const { event, owner, otherAccount } = await loadFixture(deployFixture); + + const tokenId = 0; + const uri = await event.uri(tokenId); + await expect(event.metadataUpdated(tokenId)) + .to.emit(event, "URI") + .withArgs(uri, tokenId); + }); + + // Test the sync function + // Add and update each of the things we sync (ticket type, discount code, currency) + it("Should sync the ticket type", async function () { + const { event, owner, otherAccount } = await loadFixture(deployFixture); + + const newTicket = { + key: "backstage", + displayName: "backstage", + maxSupply: 100, + active: true, + locked: false, + }; + + const newPrice = { + tokenType: "backstage", + price: ethers.utils.parseEther("200"), // 2 eth + currency: "native", + currencyAddress: AddressZero, + }; + await event.syncEventData([newTicket], [newPrice], [], [], []); + + expect((await event.tokensPurchased("backstage")).toNumber()).to.equal(0); + + // Buy a ticket, count increases + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "backstage", + 1, + otherAccount.address, + "native", + { + value: ethers.utils.parseEther("200"), + } + ); + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("backstage")).toNumber()).to.equal(1); + expect((await event.tokenAmounts("backstage")).toNumber()).to.equal(100); + + // Sync again, change the price + newPrice.price = ethers.utils.parseEther("300"); + + await event.syncEventData([], [newPrice], [], [], []); + + // Buy a ticket, fails because old price + await expect( + event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "backstage", + 1, + otherAccount.address, + "native", + { + value: ethers.utils.parseEther("200"), + } + ) + ).to.be.revertedWith("Not enough bal"); + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("backstage")).toNumber()).to.equal(1); + + // Buy a ticket, success because new price + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "backstage", + 1, + otherAccount.address, + "native", + { + value: ethers.utils.parseEther("300"), + } + ); + expect(await event.tokenIdCounter()).to.equal(2); + expect((await event.tokensPurchased("backstage")).toNumber()).to.equal(2); + }); + + /// Test to check that transfer is prevented when the ticket is soulbound + it("Should prevent transfer when locked", async function () { + const { event, owner, otherAccount } = await loadFixture(deployFixture); + + const updatedFree = { + key: "free", + displayName: "free", + maxSupply: 1000, + active: true, + locked: true, + }; + + event.syncEventData([updatedFree], [], [], [], []); + + // Get 2 tickets + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 1, + otherAccount.address, + "native" + ); + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "free", + 1, + otherAccount.address, + "native" + ); + + expect((await event.tokensPurchased("free")).toNumber()).to.equal(2); + let balToken0 = await event.balanceOf(otherAccount.address, 0); + let balToken1 = await event.balanceOf(otherAccount.address, 1); + expect(balToken0.toNumber()).to.equal(1); + expect(balToken1.toNumber()).to.equal(1); + + // Holder cannot transfer their own ticket because it's now soulbound. + await expect( + event + .connect(otherAccount) + .safeTransferFrom(otherAccount.address, owner.address, 0, 1, "0x") + ).to.be.revertedWith("Token type is locked"); + + balToken0 = await event.balanceOf(otherAccount.address, 0); + expect(balToken0.toNumber()).to.equal(1); + + // Unlock it, transfer should work + const updatedFreeUnlock = { + key: "free", + displayName: "free", + maxSupply: 1000, + active: true, + locked: false, + }; + + event.syncEventData([updatedFreeUnlock], [], [], [], []); + await event + .connect(otherAccount) + .safeTransferFrom(otherAccount.address, owner.address, 0, 1, "0x"); + + balToken0 = await event.balanceOf(otherAccount.address, 0); + expect(balToken0.toNumber()).to.equal(0); + + // Lock specific token itself, transfer should fail + event.setTokenLock(1, true); + await expect( + event + .connect(otherAccount) + .safeTransferFrom(otherAccount.address, owner.address, 1, 1, "0x") + ).to.be.revertedWith("Token is locked"); + + balToken1 = await event.balanceOf(otherAccount.address, 1); + expect(balToken1.toNumber()).to.equal(1); + }); + }); +}); diff --git a/test/EventCollector.ts b/test/EventCollector.ts new file mode 100644 index 0000000..5bfec5a --- /dev/null +++ b/test/EventCollector.ts @@ -0,0 +1,188 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { ethers } from "hardhat"; +import { expect } from "chai"; +const hre = require("hardhat"); + +const { AddressZero } = ethers.constants; + +describe("EventCollector", function () { + // We define a fixture to reuse the same setup in every test. + // We use loadFixture to run this setup once, snapshot that state, + // and reset Hardhat Network to that snapshot in every test. + async function deployFixture() { + // Contracts are deployed using the first signer/account by default + const [owner, otherAccount, otherAccount2] = await ethers.getSigners(); + + const MockTokenFactory = await ethers.getContractFactory("MockToken"); + const mockUSDC = await MockTokenFactory.deploy( + "USD COIN", + "USDC", + 6, + owner.address + ); + const mockUSDT = await MockTokenFactory.deploy( + "Tether USD", + "USDT", + 6, + owner.address + ); + + const EventCollector = await ethers.getContractFactory("EventCollector"); + + const eventCollector = await EventCollector.deploy( + ["usdc", "usdt"], + [mockUSDC.address, mockUSDT.address] + ); + + return { + eventCollector, + owner, + otherAccount, + otherAccount2, + mockUSDC, + mockUSDT, + }; + } + + describe("Deployment", function () { + it("Should set the right owner", async function () { + const { eventCollector, owner } = await loadFixture(deployFixture); + }); + }); + + describe("buyToken function", function () { + it("Should accept ETH payment", async function () { + const { eventCollector, owner, otherAccount, otherAccount2 } = + await loadFixture(deployFixture); + await eventCollector + .connect(owner) + .buyToken( + "NFT1", + 100, + otherAccount.address, + "native", + owner.address, + "", + [], + [], + { value: 100 } + ); + expect(await ethers.provider.getBalance(eventCollector.address)).to.equal( + 100 + ); + }); + + // Assuming you've deployed some ERC20 tokens and their contracts are known + it("Should accept ERC20 payment", async function () { + const { eventCollector, owner, otherAccount, otherAccount2, mockUSDC } = + await loadFixture(deployFixture); + + // Give owner exact # of tokens needed for ticket price. + await mockUSDC.mint(owner.address, 200); + // Approve exact # of tokens needed for ticket price. + await mockUSDC.connect(owner).approve(eventCollector.address, 200); + + await eventCollector.buyToken( + "NFT1", + 100, + owner.address, + "usdc", + owner.address, + "", + [], + [] + ); + + expect(await mockUSDC.balanceOf(eventCollector.address)).to.equal(100); + }); + }); + describe("sweepFunds function", function () { + it("Should sweep ETH funds", async function () { + const { eventCollector, owner, otherAccount } = await loadFixture( + deployFixture + ); + + const initialOwnerBalance = await ethers.provider.getBalance( + owner.address + ); + + // Buy a token with ETH + await eventCollector + .connect(otherAccount) + .buyToken( + "NFT1", + 100, + owner.address, + "native", + owner.address, + "", + [], + [], + { value: 100 } + ); + + // Ensure contract has the funds + expect(await ethers.provider.getBalance(eventCollector.address)).to.equal( + 100 + ); + + // Now sweep those funds + const tx = await eventCollector.sweepFunds(owner.address, [], []); + + // Expect contract balance to be zero after sweep + expect(await ethers.provider.getBalance(eventCollector.address)).to.equal( + 0 + ); + + const receipt = await tx.wait(); + const gasUsed = receipt.gasUsed.mul(tx.gasPrice); + + const finalOwnerBalance = await ethers.provider.getBalance(owner.address); + expect(finalOwnerBalance.add(gasUsed)).to.equal( + initialOwnerBalance.add(100) + ); + }); + + it("Should sweep ERC20 funds", async function () { + const { eventCollector, owner, otherAccount, mockUSDC } = + await loadFixture(deployFixture); + + const initialOwnerBalance = await mockUSDC.balanceOf(owner.address); + + // Mint and approve tokens + await mockUSDC.mint(otherAccount.address, 200); + await mockUSDC.connect(otherAccount).approve(eventCollector.address, 200); + + // Buy a token with ERC20 + await eventCollector + .connect(otherAccount) + .buyToken( + "NFT1", + 100, + owner.address, + "usdc", + owner.address, + "", + [], + [] + ); + + // Ensure contract has the funds + expect(await mockUSDC.balanceOf(eventCollector.address)).to.equal(100); + + // Now sweep those funds + await eventCollector.sweepFunds( + AddressZero, + [mockUSDC.address], + [owner.address] + ); + + // Expect contract balance to be zero after sweep + expect(await mockUSDC.balanceOf(eventCollector.address)).to.equal(0); + + // Expect owner to receive the swept funds + const finalOwnerBalance = await mockUSDC.balanceOf(owner.address); + expect(finalOwnerBalance).to.equal(initialOwnerBalance.add(100)); + }); + }); +}); diff --git a/test/Event_DiscountMerkle.ts b/test/Event_DiscountMerkle.ts new file mode 100644 index 0000000..998fe8e --- /dev/null +++ b/test/Event_DiscountMerkle.ts @@ -0,0 +1,449 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { eventData, priceData } from "./data"; +import { MerkleTree } from "merkletreejs"; + +const { AddressZero } = ethers.constants; + +describe("Event with Discount - Merkle", function () { + async function deployFixture() { + const [owner, otherAccount, otherAccount2] = await ethers.getSigners(); + + const MockTokenFactory = await ethers.getContractFactory("MockToken"); + const mockToken = await MockTokenFactory.deploy( + "USD COIN", + "USDC", + 0, + owner.address + ); + + const Event = await ethers.getContractFactory("Event"); + + const event = await Event.deploy( + owner.address, + eventData.uri, + eventData.details._name, + [], + [], + [], + [], + [] + ); + + priceData.priceBase[2].currencyAddress = mockToken.address; + priceData.priceBase[4].currencyAddress = mockToken.address; + await event.registerTokenType(eventData.ticketBase); + await event.registerCurrency(priceData.priceBase); + + const allowList = [ + owner, + ethers.Wallet.createRandom(), + ethers.Wallet.createRandom(), + ethers.Wallet.createRandom(), + otherAccount, + otherAccount2, + ]; + + const { keccak256 } = ethers.utils; + const leaves = allowList.map((wal) => keccak256(wal.address)); + const merkleTree = new MerkleTree(leaves, keccak256, { + sortPairs: true, + }); + + // For 20% off, we need to send in basis points for discount value of 2000. + const code20 = "some code 20 percent off"; + const discountValueBasis = 2000; + const usesLimit = 10; + const usesLimitPerAddress = 10; + // Register a discount code with merkleTree from allowList above. + const discount = [ + { + key: code20, + tokenType: "vip", + value: discountValueBasis, + maxUsesPerAddress: usesLimitPerAddress, + maxUsesTotal: usesLimit, + discountType: 0, + merkleRoot: merkleTree.getHexRoot(), + signer: AddressZero, + }, + ]; + await event.registerDiscount(discount); + + return { + event, + owner, + otherAccount, + otherAccount2, + mockToken, + merkleTree, + allowList, + code20, + discountValueBasis, + usesLimit, + usesLimitPerAddress, + }; + } + + describe("Discounts", function () { + // ***** DISCOUNT CODES ******** + it("Should allow us to buy a discounted ticket with a Merkle tree", async function () { + const { event, owner, merkleTree, code20, otherAccount } = + await loadFixture(deployFixture); + + // Get merkleProof for address + const { keccak256 } = ethers.utils; + const merkleProof = merkleTree.getHexProof( + keccak256(otherAccount.address) + ); + + // Buy vip ticket with code and 20% off expected eth. + await event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "vip", + 1, + otherAccount.address, + "native", + AddressZero, + code20, + merkleProof, + [], + { + value: ethers.utils.parseEther("0.8"), + } + ); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(1); + }); + + it("Should prevent a non allowlisted address from buying a discounted ticket", async function () { + const { event, merkleTree, code20, otherAccount } = await loadFixture( + deployFixture + ); + + const walNotOnAL = ethers.Wallet.createRandom(); + // Get merkleProof for address + const { keccak256 } = ethers.utils; + const merkleProof = merkleTree.getHexProof(keccak256(walNotOnAL.address)); + + // Buy vip ticket with code and 20% off expected eth. + await expect( + event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "vip", + 1, + walNotOnAL.address, + "native", + AddressZero, + code20, + merkleProof, + [], + { + value: ethers.utils.parseEther("0.8"), + } + ) + ).to.be.revertedWith("Not on merkle allow list"); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(0); + }); + + it("Should allow us to registrer and buy free discount", async function () { + const { + event, + merkleTree, + otherAccount, + usesLimit, + usesLimitPerAddress, + } = await loadFixture(deployFixture); + + const codefree = "some code free"; + const discountFreeValueBasis = 10000; + // Register a discount code for free pass + + const discount = [ + { + key: codefree, + tokenType: "vip", + value: discountFreeValueBasis, + maxUsesPerAddress: usesLimitPerAddress, + maxUsesTotal: usesLimit, + discountType: 0, + merkleRoot: merkleTree.getHexRoot(), + signer: AddressZero, + }, + ]; + await event.registerDiscount(discount); + + // Get merkleProof for address + const { keccak256 } = ethers.utils; + const merkleProof = merkleTree.getHexProof( + keccak256(otherAccount.address) + ); + + // Buy vip ticket with code for free. + await event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "vip", + 1, + otherAccount.address, + "native", + AddressZero, + codefree, + merkleProof, + [] + ); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(1); + }); + + it("Should allow us to buy a discounted ticket with updated code", async function () { + const { + event, + merkleTree, + otherAccount, + usesLimit, + usesLimitPerAddress, + } = await loadFixture(deployFixture); + + // Get merkleProof for address + const { keccak256 } = ethers.utils; + const merkleProof = merkleTree.getHexProof( + keccak256(otherAccount.address) + ); + + const code5Off = "some code 5 off"; + const discount5OffBasis = 500; + + const discount = [ + { + key: code5Off, + tokenType: "vip", + value: discount5OffBasis, + maxUsesPerAddress: usesLimitPerAddress, + maxUsesTotal: usesLimit, + discountType: 0, + merkleRoot: merkleTree.getHexRoot(), + signer: AddressZero, + }, + ]; + await event.registerDiscount(discount); + + // Buy vip ticket with code and 5% off expected eth. + await event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "vip", + 1, + otherAccount.address, + "native", + AddressZero, + code5Off, + merkleProof, + [], + { + value: ethers.utils.parseEther("0.95"), + } + ); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(1); + + // Fail vip ticket with code and 6% off expected eth. + await expect( + event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "vip", + 1, + otherAccount.address, + "native", + AddressZero, + code5Off, + merkleProof, + [], + { + value: ethers.utils.parseEther("0.94"), + } + ) + ).to.be.revertedWith("Not enough bal"); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(1); + }); + + it("Should allow us to registrer a code with a use limit", async function () { + const { event, merkleTree, otherAccount, otherAccount2 } = + await loadFixture(deployFixture); + + const codefree = "Limited To One Free"; + const usesLimit = 4; + const usesLimitPerAddress = 1; + // Register a discount code for free pass + const discount = [ + { + key: codefree, + tokenType: "vip", + value: 10000, + maxUsesPerAddress: usesLimitPerAddress, + maxUsesTotal: usesLimit, + discountType: 0, + merkleRoot: merkleTree.getHexRoot(), + signer: AddressZero, + }, + ]; + + await event.registerDiscount(discount); + + // Get merkleProof for address + const { keccak256 } = ethers.utils; + const merkleProof = merkleTree.getHexProof( + keccak256(otherAccount.address) + ); + + // Buy limited ticket with code for free. + await event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "vip", + 1, + otherAccount.address, + "native", + AddressZero, + codefree, + merkleProof, + [] + ); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(1); + + // Try to buy a second limited ticket with code for free, should fail. + await expect( + event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "vip", + 1, + otherAccount.address, + "native", + AddressZero, + codefree, + merkleProof, + [] + ) + ).to.be.revertedWith("Max uses reached for address"); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(1); + + const merkleProof2 = merkleTree.getHexProof( + keccak256(otherAccount2.address) + ); + // Try to buy a second limited ticket with code for free with another account + // , should work, below total and per address + await event + .connect(otherAccount2) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "vip", + 1, + otherAccount2.address, + "native", + AddressZero, + codefree, + merkleProof2, + [] + ); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(2); + + await expect( + event + .connect(otherAccount2) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "vip", + 4, + otherAccount2.address, + "native", + AddressZero, + codefree, + merkleProof2, + [] + ) + ).to.be.revertedWith("Max uses total reached"); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(2); + }); + + it("Should work with discounts and USDC", async function () { + const { + event, + mockToken, + merkleTree, + otherAccount, + usesLimit, + usesLimitPerAddress, + } = await loadFixture(deployFixture); + + const code15 = "media15"; + const discount15Basis = 1500; + + // Give owner exact # of tokens needed for ticket price. + const discountPrice = 2000000 * (1 - discount15Basis / 10000); + await mockToken.mint(otherAccount.address, discountPrice); + + // Approve exact # of tokens needed for ticket price. + await mockToken + .connect(otherAccount) + .approve(event.address, discountPrice); + + // Register a discount code + + const discount = [ + { + key: code15, + tokenType: "premium", + value: discount15Basis, + maxUsesPerAddress: usesLimitPerAddress, + maxUsesTotal: usesLimit, + discountType: 0, + merkleRoot: merkleTree.getHexRoot(), + signer: AddressZero, + }, + ]; + await event.registerDiscount(discount); + + // Get merkleProof for address + const { keccak256 } = ethers.utils; + const merkleProof = merkleTree.getHexProof( + keccak256(otherAccount.address) + ); + + // Buy 1 ticket of type 2 (usdc) with enough usdc + await event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "premium", + 1, + otherAccount.address, + "usdc", + AddressZero, + code15, + merkleProof, + [] + ); + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(1); + }); + }); +}); diff --git a/test/Event_DiscountSig.ts b/test/Event_DiscountSig.ts new file mode 100644 index 0000000..fab9302 --- /dev/null +++ b/test/Event_DiscountSig.ts @@ -0,0 +1,286 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { arrayify, defaultAbiCoder, keccak256 } from "ethers/lib/utils"; +import { eventData, priceData } from "./data"; + +const { AddressZero } = ethers.constants; + +describe("Event with Discount - Signature", function () { + async function deployFixture() { + const [owner, otherAccount, otherAccount2, backendSigner] = + await ethers.getSigners(); + + const MockTokenFactory = await ethers.getContractFactory("MockToken"); + const mockToken = await MockTokenFactory.deploy( + "USD COIN", + "USDC", + 0, + owner.address + ); + + const Event = await ethers.getContractFactory("Event"); + + priceData.priceBase[2].currencyAddress = mockToken.address; + priceData.priceBase[4].currencyAddress = mockToken.address; + + // For 20% off, we need to send in basis points for discount value of 2000. + const code20 = "some code 20 percent off"; + const discountValueBasis = 2000; + const usesLimit = 10; + const usesLimitPerAddress = 10; + + // For gate pass. + const codeAccess0 = "codeGate0"; + const discountValueBasis0 = 2000; + // Register a discount code with signature from allowList above. + const discounts = [ + { + key: code20, + tokenType: "vip", + value: discountValueBasis, + maxUsesPerAddress: usesLimitPerAddress, + maxUsesTotal: usesLimit, + discountType: 1, // 1 = Signature + merkleRoot: ethers.constants.HashZero, + signer: backendSigner.address, + }, + { + key: codeAccess0, // Just a gate to unlock, no discount value. + tokenType: "gated", + value: discountValueBasis0, + maxUsesPerAddress: usesLimitPerAddress, + maxUsesTotal: usesLimit, + discountType: 1, // 1 = Signature + merkleRoot: ethers.constants.HashZero, + signer: backendSigner.address, + }, + ]; + + const event = await Event.deploy( + owner.address, + eventData.uri, + eventData.details._name, + eventData.ticketBase, + priceData.priceBase, + discounts, + [], + [] + ); + + return { + event, + owner, + otherAccount, + otherAccount2, + backendSigner, + mockToken, + code20, + codeAccess0, + discountValueBasis, + usesLimit, + usesLimitPerAddress, + }; + } + + describe("Discounts", function () { + // ***** DISCOUNT CODES ******** + it("Should allow us to buy a discounted ticket with a Signature", async function () { + const { event, backendSigner, code20, otherAccount } = await loadFixture( + deployFixture + ); + + const messageToSign = keccak256( + defaultAbiCoder.encode(["address"], [otherAccount.address]) + ); + const signature = await backendSigner.signMessage( + arrayify(messageToSign) + ); + + // Buy vip ticket with code and 20% off expected eth. + await event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "vip", + 1, + otherAccount.address, + "native", + AddressZero, + code20, + [], + signature, + { + value: ethers.utils.parseEther("0.8"), + } + ); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(1); + }); + + it("Should prevent someone without an AL Signature from buying", async function () { + const { event, backendSigner, code20, otherAccount, otherAccount2 } = + await loadFixture(deployFixture); + + // Sign as otherAccount2 which is not buyer + const messageToSign = keccak256( + defaultAbiCoder.encode(["address"], [otherAccount2.address]) + ); + const signature = await backendSigner.signMessage( + arrayify(messageToSign) + ); + + // Buy vip ticket with code and 20% off expected eth. + await expect( + event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "vip", + 1, + otherAccount.address, + "native", + AddressZero, + code20, + [], + signature, + { + value: ethers.utils.parseEther("0.8"), + } + ) + ).to.be.revertedWith("Not on signature allow list"); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(0); + }); + + it("Should work with discounts and USDC", async function () { + const { + event, + mockToken, + otherAccount, + usesLimit, + usesLimitPerAddress, + backendSigner, + } = await loadFixture(deployFixture); + + const code15 = "media15"; + const discount15Basis = 1500; + + // Give owner exact # of tokens needed for ticket price. + const discountPrice = 2000000 * (1 - discount15Basis / 10000); + await mockToken.mint(otherAccount.address, discountPrice); + + // Approve exact # of tokens needed for ticket price. + await mockToken + .connect(otherAccount) + .approve(event.address, discountPrice); + + // Register a discount code + + const discount = [ + { + key: code15, + tokenType: "premium", + value: discount15Basis, + maxUsesPerAddress: usesLimitPerAddress, + maxUsesTotal: usesLimit, + discountType: 1, + merkleRoot: ethers.constants.HashZero, + signer: backendSigner.address, + }, + ]; + await event.registerDiscount(discount); + + // Get signature for address + const messageToSign = keccak256( + defaultAbiCoder.encode(["address"], [otherAccount.address]) + ); + const signature = await backendSigner.signMessage( + arrayify(messageToSign) + ); + + // Buy 1 ticket of type 2 (usdc) with enough usdc + await event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "premium", + 1, + otherAccount.address, + "usdc", + AddressZero, + code15, + [ethers.constants.HashZero], + signature + ); + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(1); + }); + + it("Should block an access gated ticket and allow with discount", async function () { + const { event, backendSigner, codeAccess0, otherAccount } = + await loadFixture(deployFixture); + + const messageToSign = keccak256( + defaultAbiCoder.encode(["address"], [otherAccount.address]) + ); + const signature = await backendSigner.signMessage( + arrayify(messageToSign) + ); + + // Buy gated without discount + await expect( + event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "gated", + 1, + otherAccount.address, + "native", + AddressZero, + "", + [], + [], + { + value: ethers.utils.parseEther("1"), + } + ) + ).to.be.revertedWith("Token type is gated"); + expect((await event.tokensPurchased("gated")).toNumber()).to.equal(0); + + // Buy gated with discount + await event + .connect(otherAccount) + [ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]( + "gated", + 1, + otherAccount.address, + "native", + AddressZero, + codeAccess0, + [], + signature, + { + value: ethers.utils.parseEther("1"), + } + ); + expect((await event.tokensPurchased("gated")).toNumber()).to.equal(1); + }); + + it("Should allow an owner to bypass the Signature requirement", async function () { + const { event, owner } = await loadFixture(deployFixture); + + // Buy gated ticket with no code and no eth + await event[ + "buyToken(string,uint256,address,string,address,string,bytes32[],bytes)" + ]("gated", 1, owner.address, "native", AddressZero, "", [], []); + + expect((await event.tokensPurchased("gated")).toNumber()).to.equal(1); + }); + }); +}); diff --git a/test/Event_PaidSingle.ts b/test/Event_PaidSingle.ts new file mode 100644 index 0000000..2883d8a --- /dev/null +++ b/test/Event_PaidSingle.ts @@ -0,0 +1,118 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { eventData, priceData } from "./data_paidSingle"; +const { AddressZero } = ethers.constants; + +describe("Event Paid - Single", function () { + async function deployFixture() { + const [owner, otherAccount] = await ethers.getSigners(); + + const MockTokenFactory = await ethers.getContractFactory("MockToken"); + const mockToken = await MockTokenFactory.deploy( + "USD COIN", + "USDC", + 0, + owner.address + ); + + const Event = await ethers.getContractFactory("Event"); + + const event = await Event.deploy( + owner.address, + eventData.uri, + eventData.details._name, + [], + [], + [], + [], + [] + ); + + priceData.priceBase[0].currencyAddress = mockToken.address; + await event.registerTokenType(eventData.ticketBase); + await event.registerCurrency(priceData.priceBase); + + // await event.registerTickets( + // eventData.ticketIds, + // eventData.ticketNames, + // eventData.amounts + // ); + + // await event.registerCurrency( + // priceData.tickets, + // priceData.costs, + // priceData.currencies, + // [mockToken.address] + // ); + + return { event, owner, otherAccount, mockToken }; + } + + describe("Deployment", function () { + it("Should set up ticket types", async function () { + const { event } = await loadFixture(deployFixture); + + // Amount of tickets available for each type is set. + expect((await event.tokenAmounts("premium")).toNumber()).to.equal( + eventData.amounts[0] + ); + }); + + it("Should prevent buying a native premium ticket for free", async function () { + const { event, otherAccount } = await loadFixture(deployFixture); + + await expect( + event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "premium", + 1, + otherAccount.address, + "native" + ) + ).to.be.revertedWith("Type not registered"); + + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(0); + }); + + it("Should let us buy with usdc", async function () { + const { event, owner, mockToken, otherAccount } = await loadFixture( + deployFixture + ); + + // Give owner exact # of tokens needed for ticket price. + await mockToken.mint(otherAccount.address, 2000000); + + // Approve exact # of tokens needed for ticket price. + await mockToken.connect(otherAccount).approve(event.address, 2000000); + + // Buy 1 ticket of type 0 (usdc) with enough usdc + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "premium", + 1, + otherAccount.address, + "usdc" + ); + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(1); + + // Buy another ticket of type 2 (usdc), not enough approved, should fail. + await expect( + event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "premium", + 1, + otherAccount.address, + "usdc" + ) + ).to.be.revertedWith("Not enough bal"); + + expect(await event.tokenIdCounter()).to.equal(1); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(1); + }); + }); +}); diff --git a/test/Event_Withdraw.ts b/test/Event_Withdraw.ts new file mode 100644 index 0000000..380ec59 --- /dev/null +++ b/test/Event_Withdraw.ts @@ -0,0 +1,294 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { eventData, priceData } from "./data"; + +const { AddressZero } = ethers.constants; + +describe("Event Withdraw", function () { + async function deployFixture() { + const [owner, otherAccount] = await ethers.getSigners(); + + const MockTokenFactory = await ethers.getContractFactory("MockToken"); + const mockToken = await MockTokenFactory.deploy( + "USD COIN", + "USDC", + 0, + owner.address + ); + + const Event = await ethers.getContractFactory("Event"); + + const event = await Event.deploy( + owner.address, + eventData.uri, + eventData.details._name, + [], + [], + [], + [], + [] + ); + + priceData.priceBase[2].currencyAddress = mockToken.address; + priceData.priceBase[4].currencyAddress = mockToken.address; + await event.registerTokenType(eventData.ticketBase); + await event.registerCurrency(priceData.priceBase); + + return { event, owner, otherAccount, mockToken }; + } + + describe("Deployment", function () { + it("Should allow a manager to withdrawer to withdraw funds from eth/native", async function () { + const { event, otherAccount } = await loadFixture(deployFixture); + + // Buy 1 more from the second ticket type with enough eth + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "vip", + 1, + otherAccount.address, + "native", + { + value: ethers.utils.parseEther("1"), + } + ); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(1); + + // Check to see if the contract has the eth sent + expect(await event.provider.getBalance(event.address)).to.equal( + ethers.utils.parseEther("1") + ); + + // Set withdrawer + await event.registerSplits([ + { + withdrawer: otherAccount.address, + percent: 100, + base: 100, + exists: true, + }, + ]); + + // Withdraw + // Withdraw fails for non manager / owner + await expect( + event.connect(otherAccount).sweepSplit("native") + ).to.be.revertedWith("No access"); + + // Withdraw works with registered manager + const MANAGER_ROLE = await event.MANAGER_ROLE(); + await event.setRole(otherAccount.address, MANAGER_ROLE); + + const otherAccountBalPre = await otherAccount.getBalance(); + + const tx = await event.connect(otherAccount).sweepSplit("native"); + const receipt = await tx.wait(); + const gasUsed = receipt.gasUsed.mul(receipt.effectiveGasPrice); + + const otherAccountBalPost = await otherAccount.getBalance(); + const withdrawn = otherAccountBalPost.sub( + otherAccountBalPre.sub(gasUsed) + ); + + // Check to see if the contract eth is gone + expect(await event.provider.getBalance(event.address)).to.equal( + ethers.utils.parseEther("0") + ); + + // Withdrawer holds eth. + expect(withdrawn).to.equal(ethers.utils.parseEther("1")); + }); + + it("Should allow owner to withdraw all", async function () { + const { event, owner, otherAccount, mockToken } = await loadFixture( + deployFixture + ); + + // Buy 2 from the second ticket type with enough eth + await event["buyToken(string,uint256,address,string)"]( + "vip", + 1, + owner.address, + "native", + { + value: ethers.utils.parseEther("1"), + } + ); + await event["buyToken(string,uint256,address,string)"]( + "vip", + 1, + owner.address, + "native", + { value: ethers.utils.parseEther("1") } + ); + expect((await event.tokensPurchased("vip")).toNumber()).to.equal(2); + + // Check to see if the contract has the eth sent + expect(await event.provider.getBalance(event.address)).to.equal( + ethers.utils.parseEther("2") + ); + + // Give owner exact # of tokens needed for ticket price. + await mockToken.mint(otherAccount.address, 2000000); + // Approve exact # of tokens needed for ticket price. + await mockToken.connect(otherAccount).approve(event.address, 2000000); + // Buy 1 ticket of type 2 (usdc) with enough usdc + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "premium", + 1, + otherAccount.address, + "usdc" + ); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(1); + + // Set withdrawer + const otherAccount2 = ethers.Wallet.createRandom(); + const otherAccount3 = ethers.Wallet.createRandom(); + + const splits = { + split0: { + withdrawer: otherAccount3.address, + percent: 25, + base: 100, + exists: true, + }, + + split1: { + withdrawer: otherAccount2.address, + percent: 75, + base: 100, + exists: true, + }, + }; + + await event.registerSplits([splits["split0"], splits["split1"]]); + + // Withdraw + const OWNER_ROLE = await event.OWNER_ROLE(); + await event.setRole(otherAccount.address, OWNER_ROLE); + + const otherAccountBalPre = await otherAccount.getBalance(); + + const tx = await event + .connect(otherAccount) + .sweepAll(otherAccount.address); + const receipt = await tx.wait(); + const gasUsed = receipt.gasUsed.mul(receipt.effectiveGasPrice); + + const otherAccountBalPost = await otherAccount.getBalance(); + const withdrawn = otherAccountBalPost.sub( + otherAccountBalPre.sub(gasUsed) + ); + + // Check to see if the contract eth is gone + expect(await event.provider.getBalance(event.address)).to.equal( + ethers.utils.parseEther("0") + ); + + // Manager holds eth. + expect(withdrawn).to.equal(ethers.utils.parseEther("2")); + + // Manager holds usdc. + expect(await mockToken.balanceOf(otherAccount.address)).to.equal(2000000); + }); + + it("Should allow the withdrawer to withdraw funds from usdc/erc20", async function () { + const { event, otherAccount, mockToken } = await loadFixture( + deployFixture + ); + + // Give owner exact # of tokens needed for ticket price. + await mockToken.mint(otherAccount.address, 2000000); + + // Approve exact # of tokens needed for ticket price. + await mockToken.connect(otherAccount).approve(event.address, 2000000); + + // Buy 1 ticket of type 2 (usdc) with enough usdc + await event + .connect(otherAccount) + ["buyToken(string,uint256,address,string)"]( + "premium", + 1, + otherAccount.address, + "usdc" + ); + expect((await event.tokensPurchased("premium")).toNumber()).to.equal(1); + + // Set withdrawers + const otherAccount2 = ethers.Wallet.createRandom(); + const otherAccount3 = ethers.Wallet.createRandom(); + + await event.registerSplits([ + { + withdrawer: otherAccount3.address, + percent: 25, + base: 100, + exists: true, + }, + { + withdrawer: otherAccount2.address, + percent: 75, + base: 100, + exists: true, + }, + ]); + expect(await mockToken.balanceOf(otherAccount3.address)).to.equal(0); + + // Check to see if the contract has the eth sent + expect(await mockToken.balanceOf(event.address)).to.equal(2000000); + + // Withdraw + await event.sweepSplit("usdc"); + + // Check to see if the contract eth is gone + expect(await mockToken.balanceOf(event.address)).to.equal(0); + + // Withdrawer account should now hold tokens. + expect(await mockToken.balanceOf(otherAccount3.address)).to.equal(500000); + expect(await mockToken.balanceOf(otherAccount2.address)).to.equal( + 1500000 + ); + }); + + it("Should properly iterate splits", async function () { + const { event, owner, otherAccount, mockToken } = await loadFixture( + deployFixture + ); + + // Set withdrawer + const otherAccount2 = ethers.Wallet.createRandom(); + const otherAccount3 = ethers.Wallet.createRandom(); + + const splits: any = { + split0: { + withdrawer: otherAccount3.address, + percent: 25, + base: 100, + exists: true, + }, + + split1: { + withdrawer: otherAccount2.address, + percent: 75, + base: 100, + exists: true, + }, + }; + + await event.registerSplits([splits["split0"], splits["split1"]]); + + // Check that all current splits equal the ones added above. + const currentSplits = await event.getSplits(); + + for (let i = 0; i < currentSplits.length; i++) { + expect(currentSplits[i].exists).to.equal(splits[`split${i}`].exists); + expect(currentSplits[i].percent).to.equal(splits[`split${i}`].percent); + expect(currentSplits[i].base).to.equal(splits[`split${i}`].base); + } + }); + }); +}); diff --git a/test/data.ts b/test/data.ts new file mode 100644 index 0000000..5a10b38 --- /dev/null +++ b/test/data.ts @@ -0,0 +1,128 @@ +import { BigNumber } from "ethers"; +import { ethers } from "hardhat"; +const { AddressZero } = ethers.constants; + +export const eventData = { + ticketBase: [ + { + key: "free", + displayName: "free", + maxSupply: 100, + active: true, + locked: false, + }, + { + key: "vip", + displayName: "vip", + maxSupply: 1000, + active: true, + locked: false, + gated: false, + }, + { + key: "premium", + displayName: "premium", + maxSupply: 600, + active: true, + locked: false, + gated: false, + }, + { + key: "extra", + displayName: "extra", + maxSupply: 500, + active: true, + locked: false, + gated: false, + }, + { + key: "gated", + displayName: "gated", + maxSupply: 500, + active: true, + locked: false, + gated: true, + }, + ], + ticketIds: ["free", "vip", "premium", "extra", "gated"], + ticketNames: ["free", "vip", "premium", "extra", "gated"], + amounts: [100, 1000, 600, 500, 500], + uri: "https://blocklive.io/metadata/collection", + details: { + _name: "ATX DAO Native 8/8/22", + _description: + "All you can crytpo, free drinks with this NFT. Hang out with the ATX DAO.", + _location: "Native Bar", + _start: 1662683400, + _end: 1662690600, + _host: "ATX DAO", + _thumbnail: + "https://worldtop.mypinata.cloud/ipfs/QmbnfRbGnakbaBvXXQvpiLEydTQVvhuG6qALmWHsXnXBDW", + }, +}; + +export const priceData = { + priceBase: [ + { + tokenType: "free", + price: 0, + currency: "native", + currencyAddress: AddressZero, + }, + { + tokenType: "vip", + price: BigNumber.from("1000000000000000000"), // ETH 18 decimals (1 ETH) + currency: "native", + currencyAddress: AddressZero, + }, + { + tokenType: "premium", + price: BigNumber.from("2000000"), // USD 6 decimals (2 USD) + currency: "usdc", + currencyAddress: AddressZero, + }, + { + tokenType: "extra", + price: BigNumber.from("1000000000000000000"), // ETH 18 decimals (1 ETH) + currency: "native", + currencyAddress: AddressZero, + }, + { + tokenType: "extra", + price: BigNumber.from("2000000"), // USD 6 decimals (2 USD) + currency: "usdc", + currencyAddress: AddressZero, + }, + { + tokenType: "extra", + price: BigNumber.from("1000000000000000000"), // ETH 18 decimals (1 ETH) + currency: "native", + currencyAddress: AddressZero, + }, + { + tokenType: "gated", + price: BigNumber.from("1000000000000000000"), // ETH 18 decimals (1 ETH) + currency: "native", + currencyAddress: AddressZero, + }, + ], + tickets: ["free", "vip", "premium", "extra", "gated"], + costs: [ + 0, + BigNumber.from("1000000000000000000"), // ETH 18 decimals (1 ETH) + BigNumber.from("2000000"), // USD 6 decimals (2 USD) + BigNumber.from("1000000000000000000"), // ETH 18 decimals (1 ETH) + BigNumber.from("2000000"), // USD 6 decimals (2 USD), + BigNumber.from("1000000000000000000"), // ETH 18 decimals (1 ETH) + BigNumber.from("1000000000000000000"), // ETH 18 decimals (1 ETH) + ], + currencies: [ + "native", + "native", + "usdc", + "native", + "usdc", + "native", + "native", + ], +}; diff --git a/test/data_paidSingle.ts b/test/data_paidSingle.ts new file mode 100644 index 0000000..372f8f6 --- /dev/null +++ b/test/data_paidSingle.ts @@ -0,0 +1,46 @@ +import { BigNumber } from "ethers"; +import { ethers } from "hardhat"; +const { AddressZero } = ethers.constants; + +export const eventData = { + ticketBase: [ + { + key: "premium", + displayName: "premium", + maxSupply: 2000, + active: true, + locked: false, + }, + ], + ticketIds: ["premium"], + ticketNames: ["premium"], + amounts: [2000], + uri: "https://blocklive.io/metadata/collection", + details: { + _name: "ATX DAO Native 8/8/22", + _description: + "All you can crytpo, free drinks with this NFT. Hang out with the ATX DAO.", + _location: "Native Bar", + _start: 1662683400, + _end: 1662690600, + _host: "ATX DAO", + _thumbnail: + "https://worldtop.mypinata.cloud/ipfs/QmbnfRbGnakbaBvXXQvpiLEydTQVvhuG6qALmWHsXnXBDW", + }, +}; + +export const priceData = { + priceBase: [ + { + tokenType: "premium", + price: BigNumber.from("2000000"), // USD 6 decimals (2 USD) + currency: "usdc", + currencyAddress: AddressZero, + }, + ], + tickets: ["premium"], + costs: [ + BigNumber.from("2000000"), // USD 6 decimals (2 USD) + ], + currencies: ["usdc"], +};