// 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 "../interfaces/IContentContract.sol"; import "../TokenPaymentSplitter.sol"; import "../TixSellLibraries.sol"; //import "hardhat/console.sol"; /* Ticket smart contract for a specific Event of a specific organizer (the owner) */ contract ContentTicketContract is ERC2981,ERC721,Ownable,AccessControl { bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); uint256 public _ticketIds; // 1 seul compteur pour tous les tickets peu importe le type mapping(uint256 => uint256) public nbTicketsSold; // ticketTypeId => number of tickets sold mapping(uint256 => uint256) public ticketTypeForToken; // id token => ticketTypeId 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; IContentContract public contentContract; struct TokenInfo { IERC20 paytoken; bool exists; } TokenInfo[] public AllowedCrypto; struct Ticket { uint256 ticketId; 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; // Events event NewTicket(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, address _tixSellpaymentSplitter, address _organizerContentPaymentSplitter, address _resellPaiementSplitter, address _dataFeedEURUSD, address _contentContract , string memory _name, uint96 royalty ) Ownable(initialOwner) ERC721(string.concat(_name," - SellTix.live content"), string.concat(_name," SellTixContent")) { for (uint256 i = 0; i < _admins.length; ++i) { _grantRole(ADMIN_ROLE, _admins[i]); _grantRole(DEFAULT_ADMIN_ROLE, _admins[i]); } resellPaiementSplitter = payable(_resellPaiementSplitter); tixSellpaymentSplitter = payable(_tixSellpaymentSplitter); organizerPaymentSplitter = payable(_organizerContentPaymentSplitter); dataFeed = AggregatorV3Interface( _dataFeedEURUSD ); dataFeedMatic = AggregatorV3Interface( TixSellLibrary.AGGREGATOR_V3_INTERFACE_ID ); addCurrency(IERC20(TixSellLibrary.USDT_ERC20)); addCurrency(IERC20(TixSellLibrary.USDC_ERC20)); contentContract = IContentContract(_contentContract); // 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(uint256 _ticketTypeId,address _to, uint256 _pricePerTicket) internal{ uint256 tokenId = _ticketIds; _safeMint(_to, tokenId); // hashcode : replace with value from contract initialization string memory secret = string(abi.encodePacked(contentContract.getContent().id, ":", Strings.toString(tokenId))); bytes32 hashedCode = sha256(bytes(secret)); tickets[tokenId] = Ticket( tokenId, _to, hashedCode, _pricePerTicket, block.timestamp, false, true ); nbTicketsSold[_ticketTypeId]+= 1; ticketTypeForToken[tokenId] = _ticketTypeId; _ticketIds+=1; emit NewTicket(tokenId,_to); } function getTicketsPrice(uint256 _ticketTypeId) internal view returns(uint256){ return contentContract.getContent().ticketTypes[_ticketTypeId].ticketPrice; } // Mint en USDT // Il faut tenir compte de notre pourcentage 2% //Ou alors on considère que le mint est toujours passé en euros... //Doit passer son ticket de réservation function buyTicket(uint256 _ticketTypeId,uint256 _amount,bool _withERC20,uint256 _cryptoId) external payable { TixSellContentLibrary.ContentTicketType memory theTicketType = contentContract.getContent().ticketTypes[_ticketTypeId]; require(contentContract.getContent().canceled==false,"Content is canceled"); // il faut vérifier si nb ticket est limité et si oui si la limite n'est pas dépassée require(theTicketType.nbTicketsLimited==false || theTicketType.nbTickets>=nbTicketsSold[_ticketTypeId]+_amount,"No more tickets available"); // default selltixfees if (sellTixRoyaltiesNotSet){ sellTixRoyaltieValue = contentContract.getContent().sellTixRoyaltieValue; sellTixRoyaltiesNotSet=false; } 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"); } // 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€ uint256 unitPrice = ((pricePerTicket*priceInDollars)/1e18); if (_withERC20){ // Fees if (unitPrice>=1e18){ fixFee = ((fixAmount*priceInDollars)/1e18); //Prendre en euros } } else{ uint priceToPaidMatic = (unitPrice/converted)*1e18; unitPrice = priceToPaidMatic; if (unitPrice>=1e18){ uint fixFeeDollar = ((fixAmount*priceInDollars)/1e18); //Prendre en euros fixFee = ((fixFeeDollar*100)/converted)*1e16; } } // 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){ //USDC 6 decimal on divise donc tout par 1e12 uint256 totalForTixSell = amountPercent+fixFee ; uint256 totalOrga = unitPrice - totalForTixSell; 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){ payable(tixSellpaymentSplitter).transfer(totalForTixSell); payable(organizerPaymentSplitter).transfer(totalOrga); } } mintTicket(_ticketTypeId,msg.sender,pricePerTicket); } } //Used for WEB2 users CB payment function mintTicket(uint256 _ticketTypeId,uint256 _amount,address _to) external onlyAdmin() { // il faut vérifier si nb ticket est limité et si oui si la limite n'est pas dépassée TixSellContentLibrary.ContentTicketType memory theTicketType = contentContract.getContent().ticketTypes[_ticketTypeId]; require(contentContract.getContent().canceled==false,"Content is canceled"); // il faut vérifier si nb ticket est limité et si oui si la limite n'est pas dépassée require(theTicketType.nbTicketsLimited==false || theTicketType.nbTickets>=nbTicketsSold[_ticketTypeId]+_amount,"No more tickets available"); // le amount est récupéré de la réservation uint256 amount = _amount; uint256 pricePerTicket = getTicketsPrice(_ticketTypeId); for (uint256 i = 0; i < amount; i++) { mintTicket(_ticketTypeId,_to,pricePerTicket); } } function mintTicketAdmin(uint256 _ticketTypeId,address _to,uint256 amount) external onlyAdmin() { uint256 pricePerTicket = getTicketsPrice(_ticketTypeId); for (uint256 i = 0; i < amount; i++) { mintTicket(_ticketTypeId,_to,pricePerTicket); } } function tokenURI(uint256 _tokenId) public view virtual override returns (string memory uri) { require( _ownerOf(_tokenId) != address(0), "ERC721Metadata: URI query for nonexistent token" ); //Si uri specific token on renvoi sinon on renvoi celui du ticket type uint256 ticketTypeid = ticketTypeForToken[_tokenId]; TixSellContentLibrary.ContentTicketType memory theTicketType = contentContract.getContent().ticketTypes[ticketTypeid]; if (bytes(ticketSpecificUri[_tokenId]).length>0){ return ticketSpecificUri[_tokenId]; } else{ //Bug il faut renvoyer un json contenant les infos du NFT... // return theTicketType.image; } } function getTotalTicketsSold() public view returns (uint256) { return _ticketIds; } function getTotalTicketsSoldForTicketType(uint256 _ticketTypeId) public view returns (uint256) { return nbTicketsSold[_ticketTypeId]; } 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 = ticketTypeForToken[tokenId]; TixSellContentLibrary.ContentTicketType memory theTicketType = contentContract.getContent().ticketTypes[ticketTypeid]; uint256 royaltySellable = theTicketType.royaltySellable; 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 getcontentContract() external view returns (address){ return address(contentContract); } 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) ) ); } }