├── LICENSE ├── README.md └── contracts ├── ERC2981Royalties.sol ├── ShowtimeMT.sol ├── ShowtimeV1Market.sol └── utils ├── AccessProtected.sol └── BaseRelayRecipient.sol /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Showtime 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # marketplace-v1-public 2 | 3 | ## ShowtimeV1Market.sol 4 | 5 | `ShowtimeV1Market` is the marketplace for Showtime NFTs. It will be deployed on Polygon mainnet. 6 | 7 | Users can either interact with the contract directly or through meta-transactions using the `BiconomyForwarder` method described below. 8 | 9 | Users can: 10 | 11 | - list a given amount of a token id for sale for a specific number of ERC20 tokens 12 | - cancel a listing 13 | - complete a sale (swap ERC20 tokens for the NFT listed for sale). If royalties are enabled, the corresponding portion of the ERC20 payment is transferred to the royalties recipient. Currently, we would expect this to be the creator of the NFT on Showtime. 14 | 15 | Note: sales can be partial, e.g. if N tokens are for sale in a particular listing, the buyer can purchase M tokens where `M <= N`. The listing is then updated to reflect that there are now only `N - M` tokens left for sale. 16 | 17 | The owner can: 18 | 19 | - pause the contract, which effectively prevents the completion of sales 20 | - add or remove ERC20 contract addresses that can be used to buy and sell NFTs 21 | - turn royalty payments on and off 22 | 23 | Some limitations: 24 | 25 | - it is hardcoded to only support a single `ShowtimeMT` ERC1155 contract 26 | - it only supports transactions in a configurable list of ERC20 tokens, no native currency 27 | - it only supports buying and selling at a fixed price, no auctions 28 | - listings don't have an expiration date 29 | - if we ever migrate to another NFT contract (or add support for more NFT contracts), we will need to migrate to a new marketplace 30 | - the owner has no control over NFTs or any other balance owned by the `ShowtimeV1Market` contract 31 | 32 | ## Meta-transactions 33 | 34 | Meta-transactions enable gas-less transactions (from the end user's point of view), for example: 35 | 36 | - a user wants to create an NFT 37 | - the app can prompt users to sign a message with the appropriate parameters 38 | - this message is sent to the Biconomy API 39 | - the `BiconomyForwarder` contract then interacts with `ShowtimeMT` / `ShowtimeV1Market` on behalf of the end user 40 | - `ShowtimeMT` / `ShowtimeV1Market` then rely on `BaseRelayRecipient._msgSender()` to get the address of the end user (`msg.sender` would return the address of the `BiconomyForwarder`) 41 | 42 | ⚠️ this method actually trusts `BiconomyForwarder` to send the correct address as the last bytes of `msg.data`, it can not verify 43 | -------------------------------------------------------------------------------- /contracts/ERC2981Royalties.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity =0.8.7; 3 | 4 | import "@openzeppelin/contracts/interfaces/IERC2981.sol"; 5 | 6 | abstract contract ERC2981Royalties is IERC2981 { 7 | struct Royalty { 8 | address recipient; 9 | uint256 value; // as a % unit, from 0 - 10000 (2 extra 0s) for eg 25% is 2500 10 | } 11 | 12 | mapping(uint256 => Royalty) internal _royalties; // tokenId => royalty 13 | 14 | function _setTokenRoyalty( 15 | uint256 id, 16 | address recipient, 17 | uint256 value 18 | ) internal { 19 | require(value <= 100_00, "ERC2981Royalties: value too high"); 20 | _royalties[id] = Royalty(recipient, value); 21 | } 22 | 23 | function royaltyInfo(uint256 _tokenId, uint256 _salePrice) 24 | external 25 | view 26 | override 27 | returns (address receiver, uint256 royaltyAmount) 28 | { 29 | Royalty memory royalty = _royalties[_tokenId]; 30 | return (royalty.recipient, (_salePrice * royalty.value) / 100_00); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /contracts/ShowtimeMT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity =0.8.7; 3 | 4 | import "@openzeppelin/contracts/interfaces/IERC2981.sol"; 5 | import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol"; 7 | import "@openzeppelin/contracts/utils/Counters.sol"; 8 | 9 | import "./utils/AccessProtected.sol"; 10 | import "./utils/BaseRelayRecipient.sol"; 11 | import "./ERC2981Royalties.sol"; 12 | 13 | contract ShowtimeMT is ERC1155Burnable, ERC2981Royalties, AccessProtected, BaseRelayRecipient { 14 | using Counters for Counters.Counter; 15 | Counters.Counter private _tokenIds; 16 | string public baseURI = "https://gateway.pinata.cloud/ipfs/"; 17 | mapping(uint256 => string) private _hashes; 18 | 19 | constructor() ERC1155("") {} 20 | 21 | /** 22 | * @dev See {IERC165-supportsInterface}. 23 | */ 24 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, IERC165) returns (bool) { 25 | return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId); 26 | } 27 | 28 | /** 29 | * Mint + Issue Token 30 | * 31 | * @param recipient - Token will be issued to recipient 32 | * @param amount - amount of tokens to mint 33 | * @param hash - IPFS hash 34 | * @param data - additional data 35 | * @param royaltyRecipient - royalty receiver address 36 | * @param royaltyPercent - percentage of royalty 37 | */ 38 | function issueToken( 39 | address recipient, 40 | uint256 amount, 41 | string memory hash, 42 | bytes memory data, 43 | address royaltyRecipient, 44 | uint256 royaltyPercent 45 | ) public onlyMinter returns (uint256) { 46 | _tokenIds.increment(); 47 | uint256 newTokenId = _tokenIds.current(); 48 | _hashes[newTokenId] = hash; 49 | _mint(recipient, newTokenId, amount, data); 50 | if (royaltyPercent > 0) { 51 | _setTokenRoyalty(newTokenId, royaltyRecipient, royaltyPercent); 52 | } 53 | return newTokenId; 54 | } 55 | 56 | /** 57 | * Mint + Issue Token Batch 58 | * 59 | * @param recipient - Token will be issued to recipient 60 | * @param amounts - amounts of each token to mint 61 | * @param hashes - IPFS hashes 62 | * @param data - additional data 63 | * @param royaltyRecipients - royalty receiver addresses 64 | * @param royaltyPercents - percentages of royalty 65 | */ 66 | function issueTokenBatch( 67 | address recipient, 68 | uint256[] memory amounts, 69 | string[] memory hashes, 70 | bytes memory data, 71 | address[] memory royaltyRecipients, 72 | uint256[] memory royaltyPercents 73 | ) public onlyMinter returns (uint256[] memory) { 74 | require( 75 | amounts.length == hashes.length && 76 | royaltyRecipients.length == royaltyPercents.length && 77 | amounts.length == royaltyRecipients.length, 78 | "array length mismatch" 79 | ); 80 | uint256[] memory ids = new uint256[](amounts.length); 81 | for (uint256 i = 0; i < amounts.length; i++) { 82 | _tokenIds.increment(); 83 | uint256 newTokenId = _tokenIds.current(); 84 | _hashes[newTokenId] = hashes[i]; 85 | ids[i] = newTokenId; 86 | if (royaltyPercents[i] > 0) { 87 | _setTokenRoyalty(newTokenId, royaltyRecipients[i], royaltyPercents[i]); 88 | } 89 | } 90 | _mintBatch(recipient, ids, amounts, data); 91 | return ids; 92 | } 93 | 94 | /** 95 | * Set Base URI 96 | * 97 | * @param _baseURI - Base URI 98 | */ 99 | function setBaseURI(string calldata _baseURI) external onlyOwner { 100 | baseURI = _baseURI; 101 | } 102 | 103 | /** 104 | * Get Token URI 105 | * 106 | * @param tokenId - Token ID 107 | */ 108 | function uri(uint256 tokenId) public view override returns (string memory) { 109 | return string(abi.encodePacked(baseURI, _hashes[tokenId])); 110 | } 111 | 112 | /** 113 | * Set Trusted Forwarder 114 | * 115 | * @param _trustedForwarder - Trusted Forwarder address 116 | */ 117 | function setTrustedForwarder(address _trustedForwarder) external onlyAdmin { 118 | trustedForwarder = _trustedForwarder; 119 | } 120 | 121 | /** 122 | * returns the message sender 123 | */ 124 | function _msgSender() internal view override(Context, BaseRelayRecipient) returns (address) { 125 | return BaseRelayRecipient._msgSender(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /contracts/ShowtimeV1Market.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity =0.8.7; 3 | 4 | import { Pausable } from "@openzeppelin/contracts/security/Pausable.sol"; 5 | import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 6 | import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 8 | import { Address } from "@openzeppelin/contracts/utils/Address.sol"; 9 | import { Ownable, Context } from "@openzeppelin/contracts/access/Ownable.sol"; 10 | import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; 11 | 12 | import { BaseRelayRecipient } from "./utils/BaseRelayRecipient.sol"; 13 | import { ShowtimeMT } from "./ShowtimeMT.sol"; 14 | 15 | /// @title Showtime V1 Market for the Showtime ERC1155 Token 16 | /// 17 | /// This is a non-escrow marketplace that allows users to list Showtime NFTs for sale 18 | /// for a fixed price, using a configurable list of allowed ERC20 currencies. 19 | /// 20 | /// @dev listings have no expiration date, but frontends may choose to hide old listings 21 | /// 22 | /// Built with feedback from the community! ♥️ 23 | /// Big thanks to: 24 | /// aaronsebesta chejazi chiuzon garythung mariobyn_eth MarkBeylin sina_eth_ 25 | /// StErMi theramblingboy timidan_x transmissions11 William94029369 26 | contract ShowtimeV1Market is Ownable, Pausable, BaseRelayRecipient { 27 | using SafeERC20 for IERC20; 28 | using Address for address; 29 | 30 | /// the address of the ShowtimeMT NFT (ERC1155) contract 31 | ShowtimeMT public immutable nft; 32 | 33 | /// @dev listings only contain a tokenId because we are implicitly only listing tokens from the ShowtimeMT contract 34 | struct Listing { 35 | uint256 tokenId; 36 | uint256 quantity; 37 | uint256 price; 38 | IERC20 currency; 39 | address seller; 40 | } 41 | 42 | /// ============ Mutable storage ============ 43 | 44 | /// royalties payments can be turned on/off by the owner of the contract 45 | bool public royaltiesEnabled = true; 46 | 47 | /// the configurable cap on royalties, enforced during the sale (50% by default) 48 | uint256 public maxRoyaltiesBasisPoints = 50_00; 49 | 50 | /// the configurable list of accepted ERC20 contract addresses 51 | mapping(address => bool) public acceptedCurrencies; 52 | 53 | /// maps a listing id to the corresponding Listing 54 | mapping(uint256 => Listing) public listings; 55 | 56 | /// a simple counter to assign ids to new listings 57 | uint256 listingCounter; 58 | 59 | /// ============ Modifiers ============ 60 | 61 | modifier onlySeller(uint256 _id) { 62 | require(listings[_id].seller == _msgSender(), "caller not seller"); 63 | _; 64 | } 65 | 66 | modifier listingExists(uint256 _id) { 67 | require(listings[_id].seller != address(0), "listing doesn't exist"); 68 | _; 69 | } 70 | 71 | /// ============ Events ============ 72 | 73 | /// marketplace and listing related events 74 | event ListingCreated(uint256 indexed listingId, address indexed seller, uint256 indexed tokenId); 75 | event ListingDeleted(uint256 indexed listingId, address indexed seller); 76 | event RoyaltyPaid(address indexed receiver, IERC20 currency, uint256 amount); 77 | event SaleCompleted( 78 | uint256 indexed listingId, 79 | address indexed seller, 80 | address indexed buyer, 81 | address receiver, 82 | uint256 quantity 83 | ); 84 | 85 | /// admin events 86 | event AcceptedCurrencyChanged(address indexed account, address currency, bool accepted); 87 | event RoyaltiesEnabledChanged(address indexed account, bool royaltiesEnabled); 88 | event MaxRoyaltiesUpdated(address indexed account, uint256 maxRoyaltiesBasisPoints); 89 | 90 | /// ============ Constructor ============ 91 | 92 | constructor( 93 | address _nft, 94 | address _trustedForwarder, 95 | address[] memory _initialCurrencies 96 | ) { 97 | /// initialize the address of the NFT contract 98 | require(_nft.isContract(), "must be contract address"); 99 | nft = ShowtimeMT(_nft); 100 | 101 | for (uint256 i = 0; i < _initialCurrencies.length; i++) { 102 | require(_initialCurrencies[i].isContract(), "_initialCurrencies must contain contract addresses"); 103 | acceptedCurrencies[_initialCurrencies[i]] = true; 104 | } 105 | 106 | /// set the trustedForwarder only once, see BaseRelayRecipient 107 | trustedForwarder = _trustedForwarder; 108 | } 109 | 110 | /// ============ Marketplace functions ============ 111 | 112 | /// @notice `setApprovalForAll` before calling 113 | /// @notice creates a new Listing 114 | /// @param _quantity the number of tokens to be listed 115 | /// @param _price the price per token 116 | function createSale( 117 | uint256 _tokenId, 118 | uint256 _quantity, 119 | uint256 _price, 120 | address _currency 121 | ) external whenNotPaused returns (uint256 listingId) { 122 | address seller = _msgSender(); 123 | 124 | require(acceptedCurrencies[_currency], "currency not accepted"); 125 | require(_quantity > 0, "quantity must be greater than 0"); 126 | require(nft.balanceOf(seller, _tokenId) >= _quantity, "seller does not own listed quantity of tokens"); 127 | 128 | Listing memory listing = Listing({ 129 | tokenId: _tokenId, 130 | quantity: _quantity, 131 | price: _price, 132 | currency: IERC20(_currency), 133 | seller: seller 134 | }); 135 | 136 | listingId = listingCounter; 137 | listings[listingId] = listing; 138 | 139 | // no need to check for overflows here 140 | unchecked { 141 | listingCounter++; 142 | } 143 | 144 | emit ListingCreated(listingId, seller, _tokenId); 145 | } 146 | 147 | /// @notice cancel an active sale 148 | function cancelSale(uint256 _listingId) external listingExists(_listingId) onlySeller(_listingId) { 149 | delete listings[_listingId]; 150 | 151 | emit ListingDeleted(_listingId, _msgSender()); 152 | } 153 | 154 | /// @notice the seller may own fewer NFTs than the listed quantity 155 | function availableForSale(uint256 _listingId) public view listingExists(_listingId) returns (uint256) { 156 | Listing memory listing = listings[_listingId]; 157 | return Math.min(nft.balanceOf(listing.seller, listing.tokenId), listing.quantity); 158 | } 159 | 160 | /// @notice Complete a sale 161 | /// @param _quantity the number of tokens to purchase 162 | /// @param _receiver the address that will receive the NFTs 163 | /// @dev we let the transaction complete even if the currency is no longer accepted in order to avoid stuck listings 164 | function buy( 165 | uint256 _listingId, 166 | uint256 _tokenId, 167 | uint256 _quantity, 168 | uint256 _price, 169 | address _currency, 170 | address _receiver 171 | ) external listingExists(_listingId) whenNotPaused { 172 | /// 1. Checks 173 | require(_receiver != address(0), "_receiver cannot be address 0"); 174 | 175 | Listing memory listing = listings[_listingId]; 176 | 177 | // to prevent issues with block reorgs, we need to make sure that the expectations of the buyer (tokenId, 178 | // price and currency) match with the listing 179 | require(listing.tokenId == _tokenId, "_tokenId does not match listing"); 180 | require(listing.price == _price, "_price does not match listing"); 181 | require(address(listing.currency) == _currency, "_currency does not match listing"); 182 | 183 | // disable buying something from the seller for the seller 184 | // note that the seller can still buy from themselves as a gift for someone else 185 | // the difference with a transfer is that this will result in royalties being paid out 186 | require(_receiver != listing.seller, "seller is not a valid receiver address"); 187 | 188 | uint256 availableQuantity = availableForSale(_listingId); 189 | require(_quantity <= availableQuantity, "required more than available quantity"); 190 | 191 | uint256 totalPrice = listing.price * _quantity; 192 | (address royaltyReceiver, uint256 royaltyAmount) = getRoyalties(listing.tokenId, totalPrice); 193 | require(royaltyAmount <= totalPrice, "royalty amount too big"); 194 | 195 | /// 2. Effects 196 | updateListing(_listingId, availableQuantity - _quantity); 197 | 198 | emit SaleCompleted(_listingId, listing.seller, _msgSender(), _receiver, _quantity); 199 | 200 | /// 3. Interactions 201 | // transfer royalties 202 | if (royaltyAmount > 0) { 203 | emit RoyaltyPaid(royaltyReceiver, listing.currency, royaltyAmount); 204 | listing.currency.safeTransferFrom(_msgSender(), royaltyReceiver, royaltyAmount); 205 | } 206 | 207 | // the royalty amount is deducted from the price paid by the buyer 208 | listing.currency.safeTransferFrom(_msgSender(), listing.seller, totalPrice - royaltyAmount); 209 | 210 | // transfer the NFTs from the seller to the buyer 211 | nft.safeTransferFrom(listing.seller, _receiver, listing.tokenId, _quantity, ""); 212 | } 213 | 214 | /// ============ Utility functions ============ 215 | 216 | /// @notice update the listing with the remaining quantity, or delete it if newQuantity is zero 217 | function updateListing(uint256 listingId, uint256 newQuantity) private { 218 | if (newQuantity == 0) { 219 | address seller = listings[listingId].seller; 220 | delete listings[listingId]; 221 | emit ListingDeleted(listingId, seller); 222 | } else { 223 | listings[listingId].quantity = newQuantity; 224 | } 225 | } 226 | 227 | function getRoyalties(uint256 tokenId, uint256 price) 228 | private 229 | view 230 | returns (address receiver, uint256 royaltyAmount) 231 | { 232 | if (!royaltiesEnabled) { 233 | return (address(0), 0); 234 | } 235 | 236 | (receiver, royaltyAmount) = nft.royaltyInfo(tokenId, price); 237 | 238 | // we ignore royalties to address 0, otherwise the transfer would fail 239 | // and it would result in NFTs that are impossible to sell 240 | if (receiver == address(0) || royaltyAmount == 0) { 241 | return (address(0), 0); 242 | } 243 | 244 | royaltyAmount = capRoyalties(price, royaltyAmount); 245 | } 246 | 247 | function capRoyalties(uint256 salePrice, uint256 royaltyAmount) private view returns (uint256) { 248 | uint256 maxRoyaltiesAmount = (salePrice * maxRoyaltiesBasisPoints) / 100_00; 249 | return Math.min(maxRoyaltiesAmount, royaltyAmount); 250 | } 251 | 252 | function _msgSender() internal view override(Context, BaseRelayRecipient) returns (address) { 253 | return BaseRelayRecipient._msgSender(); 254 | } 255 | 256 | /// ============ Admin functions ============ 257 | 258 | /// @notice switch royalty payments on/off 259 | function setRoyaltiesEnabled(bool newValue) external onlyOwner { 260 | royaltiesEnabled = newValue; 261 | 262 | emit RoyaltiesEnabledChanged(_msgSender(), royaltiesEnabled); 263 | } 264 | 265 | /// @notice sets the maximum royalties that will be paid during sales, in basis points 266 | /// ex: if a token requests 75% royalties but maxRoyaltiesBasisPoints is set to 60_00 (= 60%), 267 | /// then 60% will be paid out instead of the 75% requested 268 | function setMaxRoyalties(uint256 newValue) external onlyOwner { 269 | require(newValue <= 100_00, "maxRoyaltiesBasisPoints must be <= 100%"); 270 | maxRoyaltiesBasisPoints = newValue; 271 | 272 | emit MaxRoyaltiesUpdated(_msgSender(), maxRoyaltiesBasisPoints); 273 | } 274 | 275 | /// @notice add a currency to the accepted currency list 276 | function setAcceptedCurrency(address currency, bool accepted) external onlyOwner { 277 | require(currency.isContract(), "_currency != contract address"); 278 | acceptedCurrencies[currency] = accepted; 279 | 280 | emit AcceptedCurrencyChanged(_msgSender(), currency, accepted); 281 | } 282 | 283 | /// @notice pause the contract 284 | function pause() external whenNotPaused onlyOwner { 285 | _pause(); 286 | } 287 | 288 | /// @notice unpause the contract 289 | function unpause() external whenPaused onlyOwner { 290 | _unpause(); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /contracts/utils/AccessProtected.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity =0.8.7; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "@openzeppelin/contracts/utils/Context.sol"; 6 | 7 | abstract contract AccessProtected is Context, Ownable { 8 | mapping(address => bool) private _admins; // user address => admin? mapping 9 | mapping(address => bool) private _minters; // user address => minter? mapping 10 | bool public publicMinting; 11 | 12 | event UserAccessSet(address _user, string _access, bool _enabled); 13 | 14 | /** 15 | * @notice Set Admin Access 16 | * 17 | * @param admin - Address of Minter 18 | * @param enabled - Enable/Disable Admin Access 19 | */ 20 | function setAdmin(address admin, bool enabled) external onlyOwner { 21 | require(admin != address(0), "Invalid Admin Address"); 22 | _admins[admin] = enabled; 23 | emit UserAccessSet(admin, "ADMIN", enabled); 24 | } 25 | 26 | /** 27 | * @notice Set Minter Access 28 | * 29 | * @param minter - Address of Minter 30 | * @param enabled - Enable/Disable Admin Access 31 | */ 32 | function setMinter(address minter, bool enabled) public onlyAdmin { 33 | require(minter != address(0), "Invalid Minter Address"); 34 | _minters[minter] = enabled; 35 | emit UserAccessSet(minter, "MINTER", enabled); 36 | } 37 | 38 | /** 39 | * @notice Set Minter Access 40 | * 41 | * @param minters - Address of Minters 42 | * @param enabled - Enable/Disable Admin Access 43 | */ 44 | function setMinters(address[] calldata minters, bool enabled) external onlyAdmin { 45 | for (uint256 i = 0; i < minters.length; i++) { 46 | address minter = minters[i]; 47 | setMinter(minter, enabled); 48 | } 49 | } 50 | 51 | /** 52 | * @notice Enable/Disable public Minting 53 | * 54 | * @param enabled - Enable/Disable 55 | */ 56 | function setPublicMinting(bool enabled) external onlyAdmin { 57 | publicMinting = enabled; 58 | emit UserAccessSet(address(0), "MINTER", enabled); 59 | } 60 | 61 | /** 62 | * @notice Check Admin Access 63 | * 64 | * @param admin - Address of Admin 65 | * @return whether minter has access 66 | */ 67 | function isAdmin(address admin) public view returns (bool) { 68 | return _admins[admin]; 69 | } 70 | 71 | /** 72 | * @notice Check Minter Access 73 | * 74 | * @param minter - Address of minter 75 | * @return whether minter has access 76 | */ 77 | function isMinter(address minter) public view returns (bool) { 78 | return _minters[minter]; 79 | } 80 | 81 | /** 82 | * Throws if called by any account other than the Admin/Owner. 83 | */ 84 | modifier onlyAdmin() { 85 | require(_admins[_msgSender()] || _msgSender() == owner(), "AccessProtected: caller is not admin"); 86 | _; 87 | } 88 | 89 | /** 90 | * Throws if called by any account other than the Minter/Admin/Owner. 91 | */ 92 | modifier onlyMinter() { 93 | require( 94 | publicMinting || _minters[_msgSender()] || _admins[_msgSender()] || _msgSender() == owner(), 95 | "AccessProtected: caller is not minter" 96 | ); 97 | _; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /contracts/utils/BaseRelayRecipient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier:MIT 2 | pragma solidity =0.8.7; 3 | 4 | /** 5 | * A base contract to be inherited by any contract that want to receive relayed transactions 6 | * A subclass must use "_msgSender()" instead of "msg.sender" 7 | */ 8 | abstract contract BaseRelayRecipient { 9 | /* 10 | * Forwarder singleton we accept calls from 11 | */ 12 | address public trustedForwarder; 13 | 14 | /* 15 | * require a function to be called through GSN only 16 | */ 17 | modifier trustedForwarderOnly() { 18 | require(msg.sender == address(trustedForwarder), "Function can only be called through the trusted Forwarder"); 19 | _; 20 | } 21 | 22 | function isTrustedForwarder(address forwarder) public view returns (bool) { 23 | return forwarder == trustedForwarder; 24 | } 25 | 26 | /** 27 | * return the sender of this call. 28 | * if the call came through our trusted forwarder, return the original sender. 29 | * otherwise, return `msg.sender`. 30 | * should be used in the contract anywhere instead of msg.sender 31 | */ 32 | function _msgSender() internal view virtual returns (address ret) { 33 | if (msg.data.length >= 24 && isTrustedForwarder(msg.sender)) { 34 | // At this point we know that the sender is a trusted forwarder, 35 | // so we trust that the last bytes of msg.data are the verified sender address. 36 | // extract sender address from the end of msg.data 37 | assembly { 38 | ret := shr(96, calldataload(sub(calldatasize(), 20))) 39 | } 40 | } else { 41 | return msg.sender; 42 | } 43 | } 44 | } 45 | --------------------------------------------------------------------------------