|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | +pragma solidity ^0.8.11; |
| 3 | + |
| 4 | +/// @author thirdweb |
| 5 | + |
| 6 | +// $$\ $$\ $$\ $$\ $$\ |
| 7 | +// $$ | $$ | \__| $$ | $$ | |
| 8 | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ |
| 9 | +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ |
| 10 | +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | |
| 11 | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | |
| 12 | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | |
| 13 | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ |
| 14 | + |
| 15 | +// Interface |
| 16 | +import "./interfaces/ILoyaltyCard.sol"; |
| 17 | + |
| 18 | +// Base |
| 19 | +import "./eip/ERC721AVirtualApproveUpgradeable.sol"; |
| 20 | + |
| 21 | +// Lib |
| 22 | +import "./lib/CurrencyTransferLib.sol"; |
| 23 | + |
| 24 | +// Extensions |
| 25 | +import "./extension/NFTMetadata.sol"; |
| 26 | +import "./extension/SignatureMintERC721Upgradeable.sol"; |
| 27 | +import "./extension/ContractMetadata.sol"; |
| 28 | +import "./extension/Ownable.sol"; |
| 29 | +import "./extension/Royalty.sol"; |
| 30 | +import "./extension/PrimarySale.sol"; |
| 31 | +import "./extension/PlatformFee.sol"; |
| 32 | +import "./extension/Multicall.sol"; |
| 33 | +import "./extension/PermissionsEnumerable.sol"; |
| 34 | +import "./extension/DefaultOperatorFiltererUpgradeable.sol"; |
| 35 | +import "./openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; |
| 36 | +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; |
| 37 | + |
| 38 | +/** |
| 39 | + * @title LoyaltyCard |
| 40 | + * |
| 41 | + * @custom:description This contract is a loyalty card NFT collection. Each NFT represents a loyalty card, and the NFT's metadata |
| 42 | + * contains the loyalty card's information. A loyalty card's metadata can be updated by an admin of the contract. |
| 43 | + * A loyalty card can be cancelled (i.e. 'burned') by its owner or an approved operator. A loyalty card can be revoked |
| 44 | + * (i.e. 'burned') without its owner's approval, by an admin of the contract. |
| 45 | + */ |
| 46 | +contract LoyaltyCard is |
| 47 | + ILoyaltyCard, |
| 48 | + ContractMetadata, |
| 49 | + Ownable, |
| 50 | + Royalty, |
| 51 | + PrimarySale, |
| 52 | + PlatformFee, |
| 53 | + Multicall, |
| 54 | + PermissionsEnumerable, |
| 55 | + ReentrancyGuardUpgradeable, |
| 56 | + DefaultOperatorFiltererUpgradeable, |
| 57 | + ERC2771ContextUpgradeable, |
| 58 | + NFTMetadata, |
| 59 | + SignatureMintERC721Upgradeable, |
| 60 | + ERC721AUpgradeable |
| 61 | +{ |
| 62 | + /*/////////////////////////////////////////////////////////////// |
| 63 | + State variables |
| 64 | + //////////////////////////////////////////////////////////////*/ |
| 65 | + |
| 66 | + /// @dev Only TRANSFER_ROLE holders can have tokens transferred from or to them, during restricted transfers. |
| 67 | + bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); |
| 68 | + /// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s. |
| 69 | + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); |
| 70 | + /// @dev Only METADATA_ROLE holders can update NFT metadata. |
| 71 | + bytes32 private constant METADATA_ROLE = keccak256("METADATA_ROLE"); |
| 72 | + /// @dev Only REVOKE_ROLE holders can revoke a loyalty card. |
| 73 | + bytes32 private constant REVOKE_ROLE = keccak256("REVOKE_ROLE"); |
| 74 | + |
| 75 | + /// @dev Max bps in the thirdweb system. |
| 76 | + uint256 private constant MAX_BPS = 10_000; |
| 77 | + |
| 78 | + /*/////////////////////////////////////////////////////////////// |
| 79 | + Constructor + initializer |
| 80 | + //////////////////////////////////////////////////////////////*/ |
| 81 | + |
| 82 | + constructor() initializer {} |
| 83 | + |
| 84 | + /// @dev Initiliazes the contract, like a constructor. |
| 85 | + function initialize( |
| 86 | + address _defaultAdmin, |
| 87 | + string memory _name, |
| 88 | + string memory _symbol, |
| 89 | + string memory _contractURI, |
| 90 | + address[] memory _trustedForwarders, |
| 91 | + address _saleRecipient, |
| 92 | + address _royaltyRecipient, |
| 93 | + uint128 _royaltyBps, |
| 94 | + uint128 _platformFeeBps, |
| 95 | + address _platformFeeRecipient |
| 96 | + ) external initializer { |
| 97 | + // Initialize inherited contracts, most base-like -> most derived. |
| 98 | + __ERC2771Context_init(_trustedForwarders); |
| 99 | + __ERC721A_init(_name, _symbol); |
| 100 | + __DefaultOperatorFilterer_init(); |
| 101 | + __SignatureMintERC721_init(); |
| 102 | + __ReentrancyGuard_init(); |
| 103 | + |
| 104 | + _setupContractURI(_contractURI); |
| 105 | + _setupOwner(_defaultAdmin); |
| 106 | + _setOperatorRestriction(true); |
| 107 | + |
| 108 | + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); |
| 109 | + _setupRole(MINTER_ROLE, _defaultAdmin); |
| 110 | + _setupRole(TRANSFER_ROLE, _defaultAdmin); |
| 111 | + |
| 112 | + _setupRole(METADATA_ROLE, _defaultAdmin); |
| 113 | + _setRoleAdmin(METADATA_ROLE, METADATA_ROLE); |
| 114 | + |
| 115 | + _setupRole(REVOKE_ROLE, _defaultAdmin); |
| 116 | + _setRoleAdmin(REVOKE_ROLE, REVOKE_ROLE); |
| 117 | + |
| 118 | + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); |
| 119 | + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); |
| 120 | + _setupPrimarySaleRecipient(_saleRecipient); |
| 121 | + } |
| 122 | + |
| 123 | + /*/////////////////////////////////////////////////////////////// |
| 124 | + ERC 165 / 721 / 2981 logic |
| 125 | + //////////////////////////////////////////////////////////////*/ |
| 126 | + |
| 127 | + /// @dev Returns the URI for a given tokenId. |
| 128 | + function tokenURI(uint256 _tokenId) public view override returns (string memory) { |
| 129 | + return _getTokenURI(_tokenId); |
| 130 | + } |
| 131 | + |
| 132 | + /// @dev See ERC 165 |
| 133 | + function supportsInterface(bytes4 interfaceId) |
| 134 | + public |
| 135 | + view |
| 136 | + virtual |
| 137 | + override(ERC721AUpgradeable, IERC165) |
| 138 | + returns (bool) |
| 139 | + { |
| 140 | + return super.supportsInterface(interfaceId) || type(IERC2981).interfaceId == interfaceId; |
| 141 | + } |
| 142 | + |
| 143 | + /*/////////////////////////////////////////////////////////////// |
| 144 | + External functions |
| 145 | + //////////////////////////////////////////////////////////////*/ |
| 146 | + |
| 147 | + /// @dev Mints an NFT according to the provided mint request. Always mints 1 NFT. |
| 148 | + function mintWithSignature(MintRequest calldata _req, bytes calldata _signature) |
| 149 | + external |
| 150 | + payable |
| 151 | + nonReentrant |
| 152 | + returns (address signer) |
| 153 | + { |
| 154 | + require(_req.quantity == 1, "LoyaltyCard: only 1 NFT can be minted at a time."); |
| 155 | + |
| 156 | + signer = _processRequest(_req, _signature); |
| 157 | + address receiver = _req.to; |
| 158 | + uint256 tokenIdMinted = _mintTo(receiver, _req.uri); |
| 159 | + |
| 160 | + // Set royalties, if applicable. |
| 161 | + if (_req.royaltyRecipient != address(0) && _req.royaltyBps != 0) { |
| 162 | + _setupRoyaltyInfoForToken(tokenIdMinted, _req.royaltyRecipient, _req.royaltyBps); |
| 163 | + } |
| 164 | + |
| 165 | + _collectPrice(_req.primarySaleRecipient, _req.quantity, _req.currency, _req.pricePerToken); |
| 166 | + |
| 167 | + emit TokensMintedWithSignature(signer, receiver, tokenIdMinted, _req); |
| 168 | + } |
| 169 | + |
| 170 | + /// @dev Lets an account with MINTER_ROLE mint an NFT. Always mints 1 NFT. |
| 171 | + function mintTo(address _to, string calldata _uri) external onlyRole(MINTER_ROLE) returns (uint256 tokenIdMinted) { |
| 172 | + tokenIdMinted = _mintTo(_to, _uri); |
| 173 | + emit TokensMinted(_to, tokenIdMinted, _uri); |
| 174 | + } |
| 175 | + |
| 176 | + /// @dev Burns `tokenId`. See {ERC721-_burn}. |
| 177 | + function cancel(uint256 tokenId) external virtual override { |
| 178 | + // note: ERC721AUpgradeable's `_burn(uint256,bool)` internally checks for token approvals. |
| 179 | + _burn(tokenId, true); |
| 180 | + } |
| 181 | + |
| 182 | + /// @dev Burns `tokenId`. See {ERC721-_burn}. |
| 183 | + function revoke(uint256 tokenId) external virtual override onlyRole(REVOKE_ROLE) { |
| 184 | + _burn(tokenId); |
| 185 | + } |
| 186 | + |
| 187 | + /*/////////////////////////////////////////////////////////////// |
| 188 | + Operator filter overrides |
| 189 | + //////////////////////////////////////////////////////////////*/ |
| 190 | + |
| 191 | + /// @dev See {ERC721-setApprovalForAll}. |
| 192 | + function setApprovalForAll(address operator, bool approved) public override onlyAllowedOperatorApproval(operator) { |
| 193 | + super.setApprovalForAll(operator, approved); |
| 194 | + } |
| 195 | + |
| 196 | + /// @dev See {ERC721-approve}. |
| 197 | + function approve(address operator, uint256 tokenId) public override onlyAllowedOperatorApproval(operator) { |
| 198 | + super.approve(operator, tokenId); |
| 199 | + } |
| 200 | + |
| 201 | + /// @dev See {ERC721-_transferFrom}. |
| 202 | + function transferFrom( |
| 203 | + address from, |
| 204 | + address to, |
| 205 | + uint256 tokenId |
| 206 | + ) public override(ERC721AUpgradeable) onlyAllowedOperator(from) { |
| 207 | + super.transferFrom(from, to, tokenId); |
| 208 | + } |
| 209 | + |
| 210 | + /// @dev See {ERC721-_safeTransferFrom}. |
| 211 | + function safeTransferFrom( |
| 212 | + address from, |
| 213 | + address to, |
| 214 | + uint256 tokenId |
| 215 | + ) public override(ERC721AUpgradeable) onlyAllowedOperator(from) { |
| 216 | + super.safeTransferFrom(from, to, tokenId); |
| 217 | + } |
| 218 | + |
| 219 | + /// @dev See {ERC721-_safeTransferFrom}. |
| 220 | + function safeTransferFrom( |
| 221 | + address from, |
| 222 | + address to, |
| 223 | + uint256 tokenId, |
| 224 | + bytes memory data |
| 225 | + ) public override(ERC721AUpgradeable) onlyAllowedOperator(from) { |
| 226 | + super.safeTransferFrom(from, to, tokenId, data); |
| 227 | + } |
| 228 | + |
| 229 | + /*/////////////////////////////////////////////////////////////// |
| 230 | + Miscellaneous |
| 231 | + //////////////////////////////////////////////////////////////*/ |
| 232 | + |
| 233 | + /** |
| 234 | + * @dev Returns the total amount of tokens minted in the contract. |
| 235 | + */ |
| 236 | + function totalMinted() external view returns (uint256) { |
| 237 | + unchecked { |
| 238 | + return _currentIndex - _startTokenId(); |
| 239 | + } |
| 240 | + } |
| 241 | + |
| 242 | + /// @dev The tokenId of the next NFT that will be minted / lazy minted. |
| 243 | + function nextTokenIdToMint() external view returns (uint256) { |
| 244 | + return _currentIndex; |
| 245 | + } |
| 246 | + |
| 247 | + /*/////////////////////////////////////////////////////////////// |
| 248 | + Internal functions |
| 249 | + //////////////////////////////////////////////////////////////*/ |
| 250 | + |
| 251 | + /// @dev Collects and distributes the primary sale value of NFTs being claimed. |
| 252 | + function _collectPrice( |
| 253 | + address _primarySaleRecipient, |
| 254 | + uint256 _quantityToClaim, |
| 255 | + address _currency, |
| 256 | + uint256 _pricePerToken |
| 257 | + ) internal { |
| 258 | + if (_pricePerToken == 0) { |
| 259 | + return; |
| 260 | + } |
| 261 | + |
| 262 | + uint256 totalPrice = _quantityToClaim * _pricePerToken; |
| 263 | + |
| 264 | + bool validMsgValue; |
| 265 | + if (_currency == CurrencyTransferLib.NATIVE_TOKEN) { |
| 266 | + validMsgValue = msg.value == totalPrice; |
| 267 | + } else { |
| 268 | + validMsgValue = msg.value == 0; |
| 269 | + } |
| 270 | + require(validMsgValue, "Invalid msg value"); |
| 271 | + |
| 272 | + address saleRecipient = _primarySaleRecipient == address(0) ? primarySaleRecipient() : _primarySaleRecipient; |
| 273 | + |
| 274 | + uint256 fees; |
| 275 | + address feeRecipient; |
| 276 | + |
| 277 | + PlatformFeeType feeType = getPlatformFeeType(); |
| 278 | + if (feeType == PlatformFeeType.Flat) { |
| 279 | + (feeRecipient, fees) = getFlatPlatformFeeInfo(); |
| 280 | + } else { |
| 281 | + uint16 platformFeeBps; |
| 282 | + (feeRecipient, platformFeeBps) = getPlatformFeeInfo(); |
| 283 | + fees = (totalPrice * platformFeeBps) / MAX_BPS; |
| 284 | + } |
| 285 | + |
| 286 | + require(totalPrice >= fees, "Fees greater than price"); |
| 287 | + |
| 288 | + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), feeRecipient, fees); |
| 289 | + CurrencyTransferLib.transferCurrency(_currency, _msgSender(), saleRecipient, totalPrice - fees); |
| 290 | + } |
| 291 | + |
| 292 | + /// @dev Mints an NFT to `to` |
| 293 | + function _mintTo(address _to, string calldata _uri) internal returns (uint256 tokenIdToMint) { |
| 294 | + tokenIdToMint = _currentIndex; |
| 295 | + |
| 296 | + _setTokenURI(tokenIdToMint, _uri); |
| 297 | + _safeMint(_to, 1); |
| 298 | + } |
| 299 | + |
| 300 | + /// @dev See {ERC721-_beforeTokenTransfer}. |
| 301 | + function _beforeTokenTransfers( |
| 302 | + address from, |
| 303 | + address to, |
| 304 | + uint256 startTokenId, |
| 305 | + uint256 quantity |
| 306 | + ) internal virtual override { |
| 307 | + super._beforeTokenTransfers(from, to, startTokenId, quantity); |
| 308 | + |
| 309 | + // if transfer is restricted on the contract, we still want to allow burning and minting |
| 310 | + if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { |
| 311 | + if (!hasRole(TRANSFER_ROLE, from) && !hasRole(TRANSFER_ROLE, to)) { |
| 312 | + revert("!Transfer-Role"); |
| 313 | + } |
| 314 | + } |
| 315 | + } |
| 316 | + |
| 317 | + /// @dev Checks whether platform fee info can be set in the given execution context. |
| 318 | + function _canSetPlatformFeeInfo() internal view override returns (bool) { |
| 319 | + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); |
| 320 | + } |
| 321 | + |
| 322 | + /// @dev Checks whether primary sale recipient can be set in the given execution context. |
| 323 | + function _canSetPrimarySaleRecipient() internal view override returns (bool) { |
| 324 | + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); |
| 325 | + } |
| 326 | + |
| 327 | + /// @dev Checks whether owner can be set in the given execution context. |
| 328 | + function _canSetOwner() internal view override returns (bool) { |
| 329 | + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); |
| 330 | + } |
| 331 | + |
| 332 | + /// @dev Checks whether royalty info can be set in the given execution context. |
| 333 | + function _canSetRoyaltyInfo() internal view override returns (bool) { |
| 334 | + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); |
| 335 | + } |
| 336 | + |
| 337 | + /// @dev Checks whether contract metadata can be set in the given execution context. |
| 338 | + function _canSetContractURI() internal view override returns (bool) { |
| 339 | + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); |
| 340 | + } |
| 341 | + |
| 342 | + /// @dev Returns whether a given address is authorized to sign mint requests. |
| 343 | + function _isAuthorizedSigner(address _signer) internal view override returns (bool) { |
| 344 | + return hasRole(MINTER_ROLE, _signer); |
| 345 | + } |
| 346 | + |
| 347 | + /// @dev Returns whether metadata can be set in the given execution context. |
| 348 | + function _canSetMetadata() internal view virtual override returns (bool) { |
| 349 | + return hasRole(METADATA_ROLE, _msgSender()); |
| 350 | + } |
| 351 | + |
| 352 | + /// @dev Returns whether operator restriction can be set in the given execution context. |
| 353 | + function _canSetOperatorRestriction() internal virtual override returns (bool) { |
| 354 | + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); |
| 355 | + } |
| 356 | + |
| 357 | + function _msgSender() |
| 358 | + internal |
| 359 | + view |
| 360 | + virtual |
| 361 | + override(ContextUpgradeable, ERC2771ContextUpgradeable) |
| 362 | + returns (address sender) |
| 363 | + { |
| 364 | + return ERC2771ContextUpgradeable._msgSender(); |
| 365 | + } |
| 366 | + |
| 367 | + function _msgData() |
| 368 | + internal |
| 369 | + view |
| 370 | + virtual |
| 371 | + override(ContextUpgradeable, ERC2771ContextUpgradeable) |
| 372 | + returns (bytes calldata) |
| 373 | + { |
| 374 | + return ERC2771ContextUpgradeable._msgData(); |
| 375 | + } |
| 376 | +} |
0 commit comments