Branch data Line data Source code
1 : : // SPDX-License-Identifier: UNLICENSED
2 : : pragma solidity ^0.8.10;
3 : :
4 : : import "../interfaces/IERC20.sol";
5 : : import "../interfaces/ITicketNFT.sol";
6 : : import "../interfaces/IPrimaryMarket.sol";
7 : : import "../interfaces/ISecondaryMarket.sol";
8 : :
9 : : contract TicketNFT is ITicketNFT, IPrimaryMarket, ISecondaryMarket {
10 : : address _admin;
11 : : uint256 _totalSupply;
12 : : IERC20 public _purchaseToken;
13 : : mapping(uint256 => address) _approvals;
14 : : mapping(address => uint256) _balances;
15 : : mapping(uint256 => uint256) public _expiryTimes;
16 : : mapping(uint256 => string) _holderNames;
17 : : mapping(uint256 => address) _holders;
18 : : mapping(uint256 => address) _listers;
19 : : mapping(uint256 => uint256) _prices;
20 : : mapping(uint256 => bool) _ticketUsed;
21 : : uint256 immutable _purchasePrice = 100e18;
22 : : uint256 immutable _saleFee = 50;
23 : :
24 : : constructor(IERC20 purchaseToken) {
25 : : _admin = msg.sender;
26 : : _totalSupply = 0;
27 : : _purchaseToken = purchaseToken;
28 : : }
29 : :
30 : : function mint(address holder, string memory holderName) external {
31 [ + + ]: 39 : require(
32 : : msg.sender == address(this),
33 : : "mint: can only be called by primary market"
34 : : ); // Limits to being called by this contract and only purchase function calls mint
35 : 38 : uint256 ticketId = _totalSupply + 1;
36 : 38 : _balances[holder] += 1;
37 : 38 : _expiryTimes[ticketId] = block.timestamp + 10 days;
38 : 38 : _holderNames[ticketId] = holderName;
39 : 38 : _holders[ticketId] = holder;
40 : 38 : _ticketUsed[ticketId] = false;
41 : 38 : _totalSupply = ticketId;
42 : 38 : emit Transfer(address(0), holder, ticketId);
43 : : }
44 : :
45 : : function balanceOf(address holder) external view returns (uint256) {
46 : 48 : return _balances[holder];
47 : : }
48 : :
49 : : function holderOf(uint256 ticketID) external view returns (address) {
50 [ + + ]: 17 : require(ticketID <= _totalSupply, "holderOf: ticket doesn't exist");
51 : 16 : return _holders[ticketID];
52 : : }
53 : :
54 : : function transferFrom(
55 : : address from,
56 : : address to,
57 : : uint256 ticketID
58 : : ) external {
59 [ + + ]: 22 : require(from != address(0), "transferFrom: from cannot be 0");
60 [ + + ]: 20 : require(to != address(0), "transferFrom: to cannot be 0");
61 [ + + ]: 18 : require(
62 : : msg.sender == _holders[ticketID] ||
63 : : msg.sender == _approvals[ticketID] ||
64 : : (msg.sender == address(this) &&
65 : : (from == _holders[ticketID] ||
66 : : from == _approvals[ticketID])),
67 : : "transferFrom: msg.sender must be current holder or approved sender"
68 : : );
69 [ + + ]: 16 : require(
70 : : from == _holders[ticketID],
71 : : "transferFrom: ticket not owned by from"
72 : : );
73 : 14 : _approvals[ticketID] = address(0);
74 : 14 : _balances[from] -= 1;
75 : 14 : _balances[to] += 1;
76 : 14 : _holders[ticketID] = to;
77 : 14 : emit Transfer(from, to, ticketID);
78 : 14 : emit Approval(to, address(0), ticketID);
79 : : }
80 : :
81 : : function approve(address to, uint256 ticketID) external {
82 [ + + ]: 6 : require(ticketID <= _totalSupply, "approve: ticket doesn't exist");
83 [ + + ]: 5 : require(
84 : : msg.sender == _holders[ticketID],
85 : : "approve: msg.sender doesn't own ticket"
86 : : );
87 : 4 : _approvals[ticketID] = to;
88 : 4 : emit Approval(msg.sender, to, ticketID);
89 : : }
90 : :
91 : : function getApproved(uint256 ticketID) external view returns (address) {
92 [ + + ]: 14 : require(ticketID <= _totalSupply, "getApproved: ticket doesn't exist");
93 : 13 : return _approvals[ticketID];
94 : : }
95 : :
96 : : function holderNameOf(uint256 ticketID)
97 : : external
98 : : view
99 : : returns (string memory)
100 : : {
101 [ + + ]: 13 : require(ticketID <= _totalSupply, "holderNameOf: ticket doesn't exist");
102 : 12 : return _holderNames[ticketID];
103 : : }
104 : :
105 : : function updateHolderName(uint256 ticketID, string calldata newName)
106 : : external
107 : : {
108 [ + + ]: 3 : require(
109 : : ticketID <= _totalSupply,
110 : : "updateHolderName: ticket doesn't exist"
111 : : );
112 [ + + ]: 2 : require(
113 : : msg.sender == _holders[ticketID],
114 : : "updateHolderName: msg.sender doesn't own ticket"
115 : : );
116 : 1 : _holderNames[ticketID] = newName;
117 : : }
118 : :
119 : : function setUsed(uint256 ticketID) external {
120 [ + + ]: 8 : require(msg.sender == _admin, "setUsed: only admin can setUsed");
121 [ + + ]: 7 : require(ticketID <= _totalSupply, "setUsed: ticket doesn't exist");
122 [ + + ]: 6 : require(_ticketUsed[ticketID] == false, "setUsed: ticket already used");
123 [ + + ]: 5 : require(
124 : : _expiryTimes[ticketID] >= block.timestamp,
125 : : "setUsed: ticket expired"
126 : : );
127 : 4 : _ticketUsed[ticketID] = true;
128 : : }
129 : :
130 : : function isExpiredOrUsed(uint256 ticketID) external view returns (bool) {
131 [ + + ]: 18 : require(
132 : : ticketID <= _totalSupply,
133 : : "isExpiredOrUsed: ticket doesn't exist"
134 : : );
135 : 17 : return (_ticketUsed[ticketID] ||
136 : : _expiryTimes[ticketID] < block.timestamp);
137 : : }
138 : :
139 : : function admin() external view returns (address) {
140 : 1 : return _admin;
141 : : }
142 : :
143 : : function purchase(string memory holderName) external {
144 [ + + ]: 40 : require(_totalSupply < 1000, "purchase: maximum tickets reached");
145 [ + + ]: 39 : require(
146 : : _purchaseToken.allowance(msg.sender, address(this)) >=
147 : : _purchasePrice,
148 : : "purchase: insufficient token allowance"
149 : : );
150 : 38 : _purchaseToken.transferFrom(msg.sender, _admin, _purchasePrice);
151 : 37 : this.mint(msg.sender, holderName);
152 : 37 : emit Purchase(msg.sender, holderName);
153 : : }
154 : :
155 : : function listTicket(uint256 ticketID, uint256 price) external {
156 [ + + ]: 13 : require(
157 : : msg.sender == _holders[ticketID],
158 : : "listTicket: msg.sender doesn't own ticket"
159 : : );
160 [ + + ]: 12 : require(
161 : : _ticketUsed[ticketID] == false &&
162 : : _expiryTimes[ticketID] >= block.timestamp,
163 : : "listTicket: ticket is expired/used"
164 : : );
165 [ + + ]: 10 : require(price > 0, "listTicket: price cannot be 0");
166 : 9 : _listers[ticketID] = msg.sender;
167 : 9 : _prices[ticketID] = price;
168 : 9 : this.transferFrom(msg.sender, address(this), ticketID);
169 : 9 : emit Listing(ticketID, msg.sender, price);
170 : : }
171 : :
172 : : function purchase(uint256 ticketID, string calldata name) external {
173 [ + + ]: 6 : require(
174 : : _listers[ticketID] != address(0) && _prices[ticketID] != 0,
175 : : "purchase: ticket is not listed, missing lister address or price"
176 : : );
177 : 5 : uint256 ticketPrice = _prices[ticketID];
178 : 5 : uint256 ticketFee = _saleFee * (ticketPrice / 1000);
179 : 5 : uint256 ticketRevenue = ticketPrice - ticketFee;
180 [ + + ]: 5 : require(
181 : : _ticketUsed[ticketID] == false &&
182 : : _expiryTimes[ticketID] >= block.timestamp,
183 : : "purchase: ticket is expired/used"
184 : : );
185 [ + + ]: 3 : require(
186 : : _purchaseToken.allowance(msg.sender, address(this)) >= ticketPrice,
187 : : "purchase: insufficient token allowance"
188 : : );
189 : 2 : _purchaseToken.transferFrom(
190 : : msg.sender,
191 : : _listers[ticketID],
192 : : ticketRevenue
193 : : );
194 : 1 : _purchaseToken.transferFrom(msg.sender, _admin, ticketFee);
195 : 1 : this.transferFrom(address(this), msg.sender, ticketID);
196 : 1 : _listers[ticketID] = address(0);
197 : 1 : _prices[ticketID] = 0;
198 : 1 : _holderNames[ticketID] = name;
199 : 1 : emit Purchase(msg.sender, ticketID, ticketPrice, name);
200 : : }
201 : :
202 : : function delistTicket(uint256 ticketID) external {
203 [ + + ]: 3 : require(
204 : : msg.sender == _listers[ticketID],
205 : : "delistTicket: msg.sender didn't list ticket"
206 : : );
207 : 2 : _listers[ticketID] = address(0);
208 : 2 : _prices[ticketID] = 0;
209 : 2 : this.transferFrom(address(this), msg.sender, ticketID);
210 : 2 : emit Delisting(ticketID);
211 : : }
212 : : }
|