// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/common/ERC2981.sol"; import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import 'abdk-libraries-solidity/ABDKMathQuad.sol'; import "@openzeppelin/contracts/utils/Base64.sol"; import "./SellTixEventContract.sol"; import "../../interfaces/ITixSellNftTemplate.sol"; import "../../interfaces/ITicketReservationContract.sol"; import "../../interfaces/ITicketTypeContract.sol"; import "../../factories/ITicketReservationFactory.sol"; import "../../TokenPaymentSplitter.sol"; import "hardhat/console.sol"; /* Ticket smart contract for a specific Event of a specific organizer (the owner) */ contract SellTixTicketContract is ERC2981,ERC721,Ownable,AccessControl { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); address[] public admins; uint256 private _ticketIds; uint96 internal sellTixRoyaltieValue = 0; bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a; address payable public tixSellpaymentSplitter; address payable public resellPaiementSplitter; address payable public organizerPaymentSplitter; bool sellTixRoyaltiesNotSet = true; AggregatorV3Interface internal dataFeed; AggregatorV3Interface internal dataFeedMatic; SellTixEventContract public eventContract; ITicketReservationContract public ticketReservationContract; struct TokenInfo { IERC20 paytoken; bool exists; } TokenInfo[] public AllowedCrypto; ITixSellNftTemplateContract nftTemplateContract; string private eventId; struct Ticket { uint256 ticketId; uint256 ticketTypeId; address owner; bytes32 hashedTicket; // EventId:TicketType:TicketId encrypté en SHA256 uint256 pricePaid; uint256 purchasedDate; bool used; bool exists; } // Mappings mapping(uint256 => Ticket) public tickets; mapping(uint256 => string) public ticketSpecificUri; mapping(uint256 => uint256) public ticketTypesForTicket; mapping(address => mapping(uint256 => uint256)) public nbTicketForUserAndTicketTypes; // Events event NewTicket(uint256 ticketTypeId, uint256 ticketId,address owner); //1 smart contract par organisateur modifier onlyFounders() { require( hasRole(ADMIN_ROLE, msg.sender), "Only founders can do that" ); _; } modifier onlyAdmin() { require(msg.sender == owner() || hasRole(ADMIN_ROLE, msg.sender), "Only admins can do that"); _; } function getLatestData() public view returns (int) { // prettier-ignore ( /* uint80 roundID */, int answer, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/ ) = dataFeed.latestRoundData(); return answer; } function getLatestDataMaticUsd() public view returns(int){ ( /* uint80 roundID */, int answer, /*uint startedAt*/, /*uint timeStamp*/, /*uint80 answeredInRound*/ ) = dataFeedMatic.latestRoundData(); return answer; } constructor(address initialOwner, address[] memory _admins, string memory _eventId, address _tixSellpaymentSplitter, address _organizerEventPaymentSplitter, address _resellPaiementSplitter, address _dataFeedEURUSD, address _eventContract , string memory _eventName, address _nftTemplateContract, address _ticketReservationFactoryAddress, uint96 royalty ) Ownable(initialOwner) ERC721(string.concat(_eventName," - SellTix.live"), string.concat(_eventName," SellTixTickets")) { eventId = _eventId; for (uint256 i = 0; i < _admins.length; ++i) { _grantRole(ADMIN_ROLE, _admins[i]); _grantRole(DEFAULT_ADMIN_ROLE, _admins[i]); } admins = _admins; resellPaiementSplitter = payable(_resellPaiementSplitter); tixSellpaymentSplitter = payable(_tixSellpaymentSplitter); organizerPaymentSplitter = payable(_organizerEventPaymentSplitter); dataFeed = AggregatorV3Interface( _dataFeedEURUSD ); //TODO CHANGE FOR PROD dataFeedMatic = AggregatorV3Interface( TixSellLibrary.AGGREGATOR_V3_INTERFACE_ID ); addCurrency(IERC20(TixSellLibrary.USDT_ERC20)); addCurrency(IERC20(TixSellLibrary.USDC_ERC20)); eventContract = SellTixEventContract(_eventContract); nftTemplateContract = ITixSellNftTemplateContract(_nftTemplateContract); //Deploy reservation cntract ITicketReservationFactory ticketReservationFactory = ITicketReservationFactory(_ticketReservationFactoryAddress); address ticketReservationContractAddress = ticketReservationFactory.deployTicketReservationContract(_admins, _eventContract); ticketReservationContract = ITicketReservationContract(ticketReservationContractAddress); // default royalties on second market of event for all tickets must be 100,200,300,..., 10 000 (for 100%) uint96 finalRoyalty = royalty; _setDefaultRoyalty(resellPaiementSplitter, finalRoyalty); } function addCurrency( IERC20 _paytoken ) public { AllowedCrypto.push( TokenInfo({ paytoken: _paytoken, exists:true }) ); } function setRoyalty(uint96 _newroyalty) public onlyFounders { sellTixRoyaltieValue = _newroyalty; } // Founders or owner can override hashed Ids for security reasons (like a hacked of current ticket id) // function setHashTicketId( // uint256 _tokenId, // bytes32 _hashedTicketId // ) public onlyFounders { // Ticket storage theTicket = tickets[_tokenId]; // theTicket.hashedTicket = _hashedTicketId; // } function getBalance() public view returns (uint256) { return address(this).balance; } function setTicketURI(uint256 _tokenId, string calldata _uri) external onlyAdmin { ticketSpecificUri[_tokenId]=_uri; } function mintTicket(address _to, uint256 _ticketTypeId,uint256 _pricePerTicket) internal{ uint256 tokenId = _ticketIds; _safeMint(_to, tokenId); // hashcode : replace with value from contract initialization string memory secret = string(abi.encodePacked(eventContract.getEvent(eventId).id, ":", Strings.toString(_ticketTypeId), ":", Strings.toString(tokenId))); bytes32 hashedCode = sha256(bytes(secret)); tickets[tokenId] = Ticket( tokenId, _ticketTypeId, _to, hashedCode, _pricePerTicket, block.timestamp, false, true ); eventContract.addTicketToListOfTicketType(eventId,_ticketTypeId,tokenId); ticketTypesForTicket[tokenId] = _ticketTypeId; nbTicketForUserAndTicketTypes[_to][_ticketTypeId] = nbTicketForUserAndTicketTypes[_to][_ticketTypeId]+1; _ticketIds+=1; emit NewTicket(_ticketTypeId,tokenId,_to); } function getTicketsPrice(uint256 _ticketTypeId) internal view returns(uint256){ TixSellLibrary.TicketType memory theTicketType = ITicketTypeContract(eventContract.getTicketTypeContract(eventId)).getTicketTypeInfo(_ticketTypeId); uint256 pricePerTicket = theTicketType.ticketPrice; if (theTicketType.earlyBid){ if (theTicketType.discountEndDate > block.timestamp){ // can apply discount pricePerTicket = theTicketType.discountPrice; } } return pricePerTicket; } // Mint paiement en USDT ou USDC //Ou alors on considère que le mint est toujours passé en euros... //Doit passer son ticket de réservation function buyTicket(string memory _reservationId,uint256 _ticketTypeId,uint256 _amount,bool _withERC20,uint256 _cryptoId) external payable { // default selltixfees if (sellTixRoyaltiesNotSet){ sellTixRoyaltieValue = eventContract.getEvent(eventId).sellTixRoyaltieValue; sellTixRoyaltiesNotSet=false; } // doit créer une réservation avec le amount voulue ticketReservationContract.createReservationNumber(_reservationId,msg.sender,_ticketTypeId,_amount, nbTicketForUserAndTicketTypes[msg.sender][_ticketTypeId]); // le amount est récupéré de la réservation uint256 amount = _amount ; IERC20 paytoken; if (_withERC20){ require(_cryptoId=priceToPaid uint256 toPayInUdsc = (priceToPaid)/1e12; // console.log("price to paid usdc ",toPayInUdsc); require(paytoken.balanceOf(msg.sender)>=toPayInUdsc,"Not enought ERC20 to pay"); } else{ uint priceToPaidMatic = (priceToPaid/converted)*1e18; // console.log("price to paid matic ",priceToPaidMatic); //console.log(msg.value); require((msg.value >= priceToPaidMatic),"not enough money"); } TixSellLibrary.TicketType memory theTicketType = ITicketTypeContract(eventContract.getTicketTypeContract(eventId)).getTicketTypeInfo(_ticketTypeId); // prendre le fixAmount en fonction du Ticket Template ID ? (Template premium) uint256 fixAmount = theTicketType.fixAmount; for (uint256 i = 0; i < amount; i++) { // Send our royalties to our payment splitter uint256 fixFee = 0; //Il faut prendre notre part et envoyer le reste 'a l'orga // Our 0.10 € fee per ticket... à partir de 1€ //Price per tickets is in 1e18 //Price inDollars is in 1e18 // console.log("pricePerTicket",pricePerTicket); // console.log("priceInDollars",priceInDollars); uint256 unitPrice = ((pricePerTicket*priceInDollars)/1e18); //console.log("unitPrice",unitPrice); // Fees if (unitPrice>=1e18){ fixFee = ((fixAmount*priceInDollars)/1e18); // exprimé en ETH 1e18 } // divide sellTixRoyaltieValue by 100 because express on 10 000 uint96 provRoyalty = sellTixRoyaltieValue / 100; uint256 amountPercent = mulDiv(provRoyalty, unitPrice, 100); // organizerPaymentSplitter et par type de ticket if (_withERC20){ if (pricePerTicket>0){ // Price are expressed in 1e18 need to convert to 10e6 uint256 totalForTixSell = amountPercent+fixFee ; uint256 totalOrga = unitPrice - totalForTixSell; // if ((totalOrga+totalForTixSell)!=unitPrice){ // console.log("amountPercent",amountPercent); // console.log("fixFee",fixFee); // console.log("totalForTixSell",totalForTixSell); // console.log("totalOrga",totalOrga); // console.log((totalOrga+totalForTixSell)*1e12); // console.log("unitPrice",unitPrice); // } // console.log("totalForTixSell",totalForTixSell); // console.log("totalOrga",totalOrga); require((totalOrga+totalForTixSell)==unitPrice,"Error in the split"); paytoken.transferFrom(msg.sender, tixSellpaymentSplitter, (totalForTixSell/1e12)); paytoken.transferFrom(msg.sender, organizerPaymentSplitter, (totalOrga/1e12)); } } else{ uint256 totalForTixSell = (amountPercent+fixFee); uint256 totalOrga = (unitPrice - totalForTixSell); //Send matic if (pricePerTicket>0){ //console.log("totalForTixSell ",totalForTixSell); //console.log("totalOrga ",totalOrga); payable(tixSellpaymentSplitter).transfer(totalForTixSell); payable(organizerPaymentSplitter).transfer(totalOrga); } } mintTicket(msg.sender,_ticketTypeId,pricePerTicket); } // increment totalSupply for ticketPrice eventContract.addTicketTypesNbTicketMinted(eventId,_ticketTypeId,amount); // il faut bruler la reservation ticketReservationContract.burnReservation(_reservationId); } function createReservation(string memory _reservationNumber, uint256 _ticketTypeId, uint256 _amount) external { // doit créer une réservation avec le amount voulue ticketReservationContract.createReservationNumber(_reservationNumber,msg.sender,_ticketTypeId,_amount, nbTicketForUserAndTicketTypes[msg.sender][_ticketTypeId]); } //Used for WEB2 users CB payment //Doit passer son ticket de réservation function mintTicket(string memory _reservationId,address _to) external onlyAdmin() { TixSellReservationLibrary.TicketReservation memory reservation = ticketReservationContract.checkReservation(_reservationId); require(reservation.exists,"Invalid reservation number"); //Verification reservation pas expire ? if(reservation.expirationDate0){ return ticketSpecificUri[_tokenId]; } else{ // use smart contract to get template SVG TixSellLibrary.NftTicketInfo memory _nftTicketInfo = TixSellLibrary.NftTicketInfo( theTicketType.templateId, _tokenId, theTicketType.hiddenuri, eventContract.getEvent(eventId).eventDate, theTicketType.ticketDesignInfo, theTicketType.freeDrink, theTicketType.priorityQueue, theTicketType.canStream, theTicketType.sellable ); return nftTemplateContract.getURI(address(this),_nftTicketInfo,canReveal); } } function getTotalTicketsSold() public view returns (uint256) { return _ticketIds; } function fetchTicketsForOwner(address _fan) public view returns (Ticket[] memory) { uint256 totalItemCount = _ticketIds; uint256 itemCount = 0; uint256 currentIndex = 0; for (uint256 i = 0; i 0, "No ether left to withdraw"); //envoi l'argent sur le spliter (bool success, ) = payable(resellPaiementSplitter).call{value: balance}(""); require(success, "Transfer failed."); } function royaltyInfo(uint256 tokenId, uint256 value) public override view returns (address receiver, uint256 royaltyAmount) { require( _ownerOf(tokenId) != address(0), "Nonexistent token" ); //Get ticketTypeId from token uint256 _ticketTypeId = ticketTypesForTicket[tokenId]; TixSellLibrary.TicketType memory theTicketType = ITicketTypeContract(eventContract.getTicketTypeContract(eventId)).getTicketTypeInfo(_ticketTypeId); uint256 royaltySellable = theTicketType.royaltySellable; //royaltyAmount = mulDiv(royaltySellable,value, 100); royaltyAmount = (value * royaltySellable) / 10000; receiver = resellPaiementSplitter; return (receiver,royaltyAmount); } function supportsInterface(bytes4 interfaceId) public view override(ERC2981,ERC721,AccessControl) returns (bool){ return interfaceId == _INTERFACE_ID_ERC2981 || super.supportsInterface(interfaceId); } function getEventContract() external view returns (address){ return address(eventContract); } function getTicketTypesForTicket(uint256 _tokenId) external view returns (uint256){ return ticketTypesForTicket[_tokenId]; } function getResellPaymentSplitter() external view returns (address){ return resellPaiementSplitter; } function mulDiv (uint x, uint y, uint z) public pure returns (uint) { return ABDKMathQuad.toUInt ( ABDKMathQuad.div ( ABDKMathQuad.mul ( ABDKMathQuad.fromUInt (x), ABDKMathQuad.fromUInt (y) ), ABDKMathQuad.fromUInt (z) ) ); } }