├── .env.sample ├── .gitignore ├── README.md ├── contracts ├── .DS_Store ├── ERC721A.sol ├── GalleryToken.sol ├── IMarketplace.sol ├── Marketplace.sol ├── Series.sol ├── TestNFT.sol ├── cards │ ├── ERC1155.sol │ ├── GeneralCards.sol │ ├── Invite1155.sol │ ├── StringUtils.sol │ └── Whitelistable.sol ├── mementos │ ├── GalleryMementos.sol │ └── GalleryMementosMultiMinter.sol └── merch │ └── MerchNFT.sol ├── hardhat.config.js ├── metadatas ├── 0.json ├── 1.json ├── 2.json ├── 3.json ├── 4.json ├── 5.json ├── 6.json ├── 7.json ├── 8.json ├── base-v2.json ├── base.json ├── beyond.json ├── blooming.json ├── card.json ├── general_001.json ├── hat.json ├── infinita.json ├── poster.json └── t-shirt.json ├── package.json ├── scripts ├── comma.js ├── general │ ├── createType.js │ ├── deploy.js │ ├── mint.js │ └── setCanMint.js ├── helpers │ └── merkleTree.js ├── mementos │ ├── canMint.js │ ├── deploy.js │ ├── getMaxSupply.js │ ├── getPrice.js │ ├── getUsedSupply.js │ ├── lower.js │ ├── mint.js │ ├── mintMany.js │ ├── setCanMint.js │ ├── setTokenType.js │ ├── uri.js │ └── withdraw.js ├── merch │ ├── deploy.js │ ├── description.js │ ├── encode.js │ ├── getReserveSupply.js │ ├── isRedeemed.js │ ├── mint.js │ ├── mintReserve.js │ ├── setCanMint.js │ ├── setMerchType.js │ ├── setReserveSupply.js │ └── withdraw.js ├── premium │ ├── balance.js │ ├── balanceOfBatch.js │ ├── createType.js │ ├── deploy.js │ ├── deployTest.js │ ├── isMintApproved.js │ ├── mintMany.js │ ├── mintTest.js │ ├── setCanMint.js │ ├── setMintApproval.js │ ├── setMintApprovals.js │ ├── setPrice.js │ ├── setWhitelistCheck.js │ ├── supply.js │ ├── uri.js │ └── withdraw.js ├── series │ ├── deploy.js │ └── mint.js └── uri.js ├── snapshot ├── allowlist.js ├── snapshot.js ├── to-snapshot.json └── validateSnapshot.js ├── test ├── General.js └── Invite.js └── yarn.lock /.env.sample: -------------------------------------------------------------------------------- 1 | API_URL=alchemy api url 2 | TEST_URL=alchemy test api url 3 | PRIVATE_KEY=private key for executing transactions 4 | PUBLIC_KEY=public key match for private 5 | TEST_PRIVATE_KEY=private key for executing testnet transactions 6 | TEST_PUBLIC_KEY=public key matching test private 7 | PREMIUM_CONTRACT_ADDRESS=0xe01569ca9b39E55Bc7C0dFa09F05fa15CB4C7698 8 | MEMENTOS_CONTRACT_ADDRESS=0x7e619a01e1a3b3a6526d0e01fbac4822d48f439b 9 | TEST_PREMIUM_CONTRACT_ADDRESS=test premium contract address 10 | GENERAL_CONTRACT_ADDRESS=0xE3d0fe9B7E0B951663267a3Ed1e6577f6f79757e 11 | TESTNET_GENERAL_CONTRACT_ADDRESS=test general contract address 12 | BASE_MEMENTOS_MULTI_CONTRACT_ADDRESS=0x53D78e00dE2F3069c997Ca3bD89f8794294aE3c9 13 | OPENSEA_API_KEY=opensea api key 14 | MERCH_CONTRACT_ADDRESS=0x01f55be815fbd10b1770b008b8960931a30e7f65 15 | BASE_MEMENTOS_CONTRACT_ADDRESS=0x1680299203930151E0De5137459A866dCf67198e 16 | SERIES_CONTRACT_ADDRESS=0xe01569ca9b39E55Bc7C0dFa09F05fa15CB4C7698 17 | GAS_PRICE=10000000000 # 10 gwei example, use a gas tracker to get the current price -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cache/ 3 | artifacts/ 4 | .env 5 | .openzeppelin/ 6 | snapshot*.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NFTs Gallery 2 | 3 | ### The official repository of the smart contracts and deployment/upgrade scripts for official Gallery NFTs. 4 | 5 | _Before doing anything, make sure you have node/npm/yarn installed and have initialized dependencies._ 6 | 7 | ## Snapshotting and Allowlisting 8 | 9 | 1. Ensure you have the most recent snapshot in a file called `snapshot/snapshot-old.json` and your `.env` file is complete. You can find the old snapshot [here](https://storage.cloud.google.com/gallery-prod-325303.appspot.com/snapshot.json) and you can use `.env.sample` to create a good `.env` file. 10 | 11 | 2. Fill out `snapshot/to-snapshot.json` with the new contracts/collections you want to snapshot as well as any manual addresses. Use the following format for each field: 12 | 13 | ```json 14 | { 15 | // to snapshot an entire ERC-721 collection using opensea 16 | "erc721addresses": [ "erc721contractAddress1", "erc721contractAddress2", ...], 17 | // if you want a specific range of token IDs from an ERC721 18 | // can duplicate contract addresses if multiple ranges within same contract 19 | "erc721TokenIDRanges": [ 20 | ["erc721contractAddress3", [0, 100]], 21 | ["erc721contractAddress4", [900, 10040]] 22 | ], 23 | // snapshot an opensea collection, great for art blocks collections because they are separated into collections on opensea but not in the contract. 24 | // the collection name is the last part of the URL on a collection page. e.g. https://opensea.io/collection/chromie-squiggle-by-snowfro -> "chromie-squiggle-by-snowfro" 25 | "openseaCollectionNames": ["collection-name-1", "collection-name-2", ...], 26 | // the owners of a single token ID in an ERC-1155 contract. For example, the Gallery Membership Card Tiers. 27 | // all ERC-1155 contracts should be here, opensea cannot handle ERC-1155 contracts 28 | // note 1: duplicate contract addresses are allowed! 29 | // note 2: when a number is too long, javascript complains. Just surround it in quotes 30 | // note 3: you can also toss in ERC-721s in here 31 | "erc1155AddressTokenIDs": [ 32 | ["erc1155contractAddress1", 5], 33 | ["erc1155contractAddress2", "10003920003940"] 34 | ], 35 | // any addresses to manually include 36 | "manualInclude": ["address1", "address2", ...] 37 | } 38 | ``` 39 | 40 | 3. Run the command: 41 | 42 | ```bash 43 | node snapshot/snapshot.js 44 | ``` 45 | 46 | 4. Ensure the `GAS_PRICE` value in your `.env` (in wei) is at least 10 gwei above the current gas price. Use [this website](https://eth-converter.com/) to convert from wei to gwei and use [this website](https://etherscan.io/gastracker) to see the current gas prices in gwei. 47 | 5. Run the command: 48 | 49 | ```bash 50 | npx hardhat run --network main snapshot/allowlist.js 51 | ``` 52 | 53 | 6. With your gallery account running in a chrome window, go to [this website](https://console.cloud.google.com/storage/browser/gallery-prod-325303.appspot.com;tab=objects?project=gallery-prod-325303&supportedpurview=project&prefix=&forceOnObjectsSortingFiltering=false) and click `Upload Files`. Upload the new `snapshot.json` file that was just generated in the `snapshot` folder of this repo and make sure to overwrite it if it already exists. 54 | 55 | Done! 56 | -------------------------------------------------------------------------------- /contracts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gallery-so/contracts/3784593ff2c13a2404a168b2aa1fdeaedf4a6e24/contracts/.DS_Store -------------------------------------------------------------------------------- /contracts/ERC721A.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Creator: Chiru Labs 3 | 4 | pragma solidity ^0.8.4; 5 | 6 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 8 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; 9 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; 10 | import "@openzeppelin/contracts/utils/Address.sol"; 11 | import "@openzeppelin/contracts/utils/Context.sol"; 12 | import "@openzeppelin/contracts/utils/Strings.sol"; 13 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; 14 | 15 | error ApprovalCallerNotOwnerNorApproved(); 16 | error ApprovalQueryForNonexistentToken(); 17 | error ApproveToCaller(); 18 | error ApprovalToCurrentOwner(); 19 | error BalanceQueryForZeroAddress(); 20 | error MintToZeroAddress(); 21 | error MintZeroQuantity(); 22 | error OwnerQueryForNonexistentToken(); 23 | error TransferCallerNotOwnerNorApproved(); 24 | error TransferFromIncorrectOwner(); 25 | error TransferToNonERC721ReceiverImplementer(); 26 | error TransferToZeroAddress(); 27 | error URIQueryForNonexistentToken(); 28 | 29 | /** 30 | * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including 31 | * the Metadata extension. Built to optimize for lower gas during batch mints. 32 | * 33 | * Assumes serials are sequentially minted starting at _startTokenId() (defaults to 0, e.g. 0, 1, 2, 3..). 34 | * 35 | * Assumes that an owner cannot have more than 2**64 - 1 (max value of uint64) of supply. 36 | * 37 | * Assumes that the maximum token id cannot exceed 2**256 - 1 (max value of uint256). 38 | */ 39 | contract ERC721A is Context, ERC165, IERC721, IERC721Metadata { 40 | using Address for address; 41 | using Strings for uint256; 42 | 43 | // Compiler will pack this into a single 256bit word. 44 | struct TokenOwnership { 45 | // The address of the owner. 46 | address addr; 47 | // Keeps track of the start time of ownership with minimal overhead for tokenomics. 48 | uint64 startTimestamp; 49 | // Whether the token has been burned. 50 | bool burned; 51 | } 52 | 53 | // Compiler will pack this into a single 256bit word. 54 | struct AddressData { 55 | // Realistically, 2**64-1 is more than enough. 56 | uint64 balance; 57 | // Keeps track of mint count with minimal overhead for tokenomics. 58 | uint64 numberMinted; 59 | // Keeps track of burn count with minimal overhead for tokenomics. 60 | uint64 numberBurned; 61 | // For miscellaneous variable(s) pertaining to the address 62 | // (e.g. number of whitelist mint slots used). 63 | // If there are multiple variables, please pack them into a uint64. 64 | uint64 aux; 65 | } 66 | 67 | // The tokenId of the next token to be minted. 68 | uint256 internal _currentIndex; 69 | 70 | // The number of tokens burned. 71 | uint256 internal _burnCounter; 72 | 73 | // Token name 74 | string private _name; 75 | 76 | // Token symbol 77 | string private _symbol; 78 | 79 | // Mapping from token ID to ownership details 80 | // An empty struct value does not necessarily mean the token is unowned. See _ownershipOf implementation for details. 81 | mapping(uint256 => TokenOwnership) internal _ownerships; 82 | 83 | // Mapping owner address to address data 84 | mapping(address => AddressData) private _addressData; 85 | 86 | // Mapping from token ID to approved address 87 | mapping(uint256 => address) private _tokenApprovals; 88 | 89 | // Mapping from owner to operator approvals 90 | mapping(address => mapping(address => bool)) private _operatorApprovals; 91 | 92 | constructor(string memory name_, string memory symbol_) { 93 | _name = name_; 94 | _symbol = symbol_; 95 | _currentIndex = _startTokenId(); 96 | } 97 | 98 | /** 99 | * To change the starting tokenId, please override this function. 100 | */ 101 | function _startTokenId() internal view virtual returns (uint256) { 102 | return 0; 103 | } 104 | 105 | /** 106 | * @dev See {IERC721Enumerable-totalSupply}. 107 | * @dev Burned tokens are calculated here, use _totalMinted() if you want to count just minted tokens. 108 | */ 109 | function totalSupply() public view returns (uint256) { 110 | // Counter underflow is impossible as _burnCounter cannot be incremented 111 | // more than _currentIndex - _startTokenId() times 112 | unchecked { 113 | return _currentIndex - _burnCounter - _startTokenId(); 114 | } 115 | } 116 | 117 | /** 118 | * Returns the total amount of tokens minted in the contract. 119 | */ 120 | function _totalMinted() internal view returns (uint256) { 121 | // Counter underflow is impossible as _currentIndex does not decrement, 122 | // and it is initialized to _startTokenId() 123 | unchecked { 124 | return _currentIndex - _startTokenId(); 125 | } 126 | } 127 | 128 | /** 129 | * @dev See {IERC165-supportsInterface}. 130 | */ 131 | function supportsInterface(bytes4 interfaceId) 132 | public 133 | view 134 | virtual 135 | override(ERC165, IERC165) 136 | returns (bool) 137 | { 138 | return 139 | interfaceId == type(IERC721).interfaceId || 140 | interfaceId == type(IERC721Metadata).interfaceId || 141 | super.supportsInterface(interfaceId); 142 | } 143 | 144 | /** 145 | * @dev See {IERC721-balanceOf}. 146 | */ 147 | function balanceOf(address owner) public view override returns (uint256) { 148 | if (owner == address(0)) revert BalanceQueryForZeroAddress(); 149 | return uint256(_addressData[owner].balance); 150 | } 151 | 152 | /** 153 | * Returns the number of tokens minted by `owner`. 154 | */ 155 | function _numberMinted(address owner) internal view returns (uint256) { 156 | return uint256(_addressData[owner].numberMinted); 157 | } 158 | 159 | /** 160 | * Returns the number of tokens burned by or on behalf of `owner`. 161 | */ 162 | function _numberBurned(address owner) internal view returns (uint256) { 163 | return uint256(_addressData[owner].numberBurned); 164 | } 165 | 166 | /** 167 | * Returns the auxillary data for `owner`. (e.g. number of whitelist mint slots used). 168 | */ 169 | function _getAux(address owner) internal view returns (uint64) { 170 | return _addressData[owner].aux; 171 | } 172 | 173 | /** 174 | * Sets the auxillary data for `owner`. (e.g. number of whitelist mint slots used). 175 | * If there are multiple variables, please pack them into a uint64. 176 | */ 177 | function _setAux(address owner, uint64 aux) internal { 178 | _addressData[owner].aux = aux; 179 | } 180 | 181 | /** 182 | * Gas spent here starts off proportional to the maximum mint batch size. 183 | * It gradually moves to O(1) as tokens get transferred around in the collection over time. 184 | */ 185 | function _ownershipOf(uint256 tokenId) 186 | internal 187 | view 188 | returns (TokenOwnership memory) 189 | { 190 | uint256 curr = tokenId; 191 | 192 | unchecked { 193 | if (_startTokenId() <= curr && curr < _currentIndex) { 194 | TokenOwnership memory ownership = _ownerships[curr]; 195 | if (!ownership.burned) { 196 | if (ownership.addr != address(0)) { 197 | return ownership; 198 | } 199 | // Invariant: 200 | // There will always be an ownership that has an address and is not burned 201 | // before an ownership that does not have an address and is not burned. 202 | // Hence, curr will not underflow. 203 | while (true) { 204 | curr--; 205 | ownership = _ownerships[curr]; 206 | if (ownership.addr != address(0)) { 207 | return ownership; 208 | } 209 | } 210 | } 211 | } 212 | } 213 | revert OwnerQueryForNonexistentToken(); 214 | } 215 | 216 | /** 217 | * @dev See {IERC721-ownerOf}. 218 | */ 219 | function ownerOf(uint256 tokenId) public view override returns (address) { 220 | return _ownershipOf(tokenId).addr; 221 | } 222 | 223 | /** 224 | * @dev See {IERC721Metadata-name}. 225 | */ 226 | function name() public view virtual override returns (string memory) { 227 | return _name; 228 | } 229 | 230 | /** 231 | * @dev See {IERC721Metadata-symbol}. 232 | */ 233 | function symbol() public view virtual override returns (string memory) { 234 | return _symbol; 235 | } 236 | 237 | /** 238 | * @dev See {IERC721Metadata-tokenURI}. 239 | */ 240 | function tokenURI(uint256 tokenId) 241 | public 242 | view 243 | virtual 244 | override 245 | returns (string memory) 246 | { 247 | if (!_exists(tokenId)) revert URIQueryForNonexistentToken(); 248 | 249 | string memory baseURI = _baseURI(); 250 | return 251 | bytes(baseURI).length != 0 252 | ? string(abi.encodePacked(baseURI, tokenId.toString())) 253 | : ""; 254 | } 255 | 256 | /** 257 | * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each 258 | * token will be the concatenation of the `baseURI` and the `tokenId`. Empty 259 | * by default, can be overriden in child contracts. 260 | */ 261 | function _baseURI() internal view virtual returns (string memory) { 262 | return ""; 263 | } 264 | 265 | /** 266 | * @dev See {IERC721-approve}. 267 | */ 268 | function approve(address to, uint256 tokenId) public override { 269 | address owner = ERC721A.ownerOf(tokenId); 270 | if (to == owner) revert ApprovalToCurrentOwner(); 271 | 272 | if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) { 273 | revert ApprovalCallerNotOwnerNorApproved(); 274 | } 275 | 276 | _approve(to, tokenId, owner); 277 | } 278 | 279 | /** 280 | * @dev See {IERC721-getApproved}. 281 | */ 282 | function getApproved(uint256 tokenId) 283 | public 284 | view 285 | override 286 | returns (address) 287 | { 288 | if (!_exists(tokenId)) revert ApprovalQueryForNonexistentToken(); 289 | 290 | return _tokenApprovals[tokenId]; 291 | } 292 | 293 | /** 294 | * @dev See {IERC721-setApprovalForAll}. 295 | */ 296 | function setApprovalForAll(address operator, bool approved) 297 | public 298 | virtual 299 | override 300 | { 301 | if (operator == _msgSender()) revert ApproveToCaller(); 302 | 303 | _operatorApprovals[_msgSender()][operator] = approved; 304 | emit ApprovalForAll(_msgSender(), operator, approved); 305 | } 306 | 307 | /** 308 | * @dev See {IERC721-isApprovedForAll}. 309 | */ 310 | function isApprovedForAll(address owner, address operator) 311 | public 312 | view 313 | virtual 314 | override 315 | returns (bool) 316 | { 317 | return _operatorApprovals[owner][operator]; 318 | } 319 | 320 | /** 321 | * @dev See {IERC721-transferFrom}. 322 | */ 323 | function transferFrom( 324 | address from, 325 | address to, 326 | uint256 tokenId 327 | ) public virtual override { 328 | _transfer(from, to, tokenId); 329 | } 330 | 331 | /** 332 | * @dev See {IERC721-safeTransferFrom}. 333 | */ 334 | function safeTransferFrom( 335 | address from, 336 | address to, 337 | uint256 tokenId 338 | ) public virtual override { 339 | safeTransferFrom(from, to, tokenId, ""); 340 | } 341 | 342 | /** 343 | * @dev See {IERC721-safeTransferFrom}. 344 | */ 345 | function safeTransferFrom( 346 | address from, 347 | address to, 348 | uint256 tokenId, 349 | bytes memory _data 350 | ) public virtual override { 351 | _transfer(from, to, tokenId); 352 | if ( 353 | to.isContract() && 354 | !_checkContractOnERC721Received(from, to, tokenId, _data) 355 | ) { 356 | revert TransferToNonERC721ReceiverImplementer(); 357 | } 358 | } 359 | 360 | /** 361 | * @dev Returns whether `tokenId` exists. 362 | * 363 | * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. 364 | * 365 | * Tokens start existing when they are minted (`_mint`), 366 | */ 367 | function _exists(uint256 tokenId) internal view returns (bool) { 368 | return 369 | _startTokenId() <= tokenId && 370 | tokenId < _currentIndex && 371 | !_ownerships[tokenId].burned; 372 | } 373 | 374 | function _safeMint(address to, uint256 quantity) internal { 375 | _safeMint(to, quantity, ""); 376 | } 377 | 378 | /** 379 | * @dev Safely mints `quantity` tokens and transfers them to `to`. 380 | * 381 | * Requirements: 382 | * 383 | * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called for each safe transfer. 384 | * - `quantity` must be greater than 0. 385 | * 386 | * Emits a {Transfer} event. 387 | */ 388 | function _safeMint( 389 | address to, 390 | uint256 quantity, 391 | bytes memory _data 392 | ) internal { 393 | _mint(to, quantity, _data, true); 394 | } 395 | 396 | /** 397 | * @dev Mints `quantity` tokens and transfers them to `to`. 398 | * 399 | * Requirements: 400 | * 401 | * - `to` cannot be the zero address. 402 | * - `quantity` must be greater than 0. 403 | * 404 | * Emits a {Transfer} event. 405 | */ 406 | function _mint( 407 | address to, 408 | uint256 quantity, 409 | bytes memory _data, 410 | bool safe 411 | ) internal { 412 | uint256 startTokenId = _currentIndex; 413 | if (to == address(0)) revert MintToZeroAddress(); 414 | if (quantity == 0) revert MintZeroQuantity(); 415 | 416 | _beforeTokenTransfers(address(0), to, startTokenId, quantity); 417 | 418 | // Overflows are incredibly unrealistic. 419 | // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1 420 | // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1 421 | unchecked { 422 | _addressData[to].balance += uint64(quantity); 423 | _addressData[to].numberMinted += uint64(quantity); 424 | 425 | _ownerships[startTokenId].addr = to; 426 | _ownerships[startTokenId].startTimestamp = uint64(block.timestamp); 427 | 428 | uint256 updatedIndex = startTokenId; 429 | uint256 end = updatedIndex + quantity; 430 | 431 | if (safe && to.isContract()) { 432 | do { 433 | emit Transfer(address(0), to, updatedIndex); 434 | if ( 435 | !_checkContractOnERC721Received( 436 | address(0), 437 | to, 438 | updatedIndex++, 439 | _data 440 | ) 441 | ) { 442 | revert TransferToNonERC721ReceiverImplementer(); 443 | } 444 | } while (updatedIndex != end); 445 | // Reentrancy protection 446 | if (_currentIndex != startTokenId) revert(); 447 | } else { 448 | do { 449 | emit Transfer(address(0), to, updatedIndex++); 450 | } while (updatedIndex != end); 451 | } 452 | _currentIndex = updatedIndex; 453 | } 454 | _afterTokenTransfers(address(0), to, startTokenId, quantity); 455 | } 456 | 457 | /** 458 | * @dev Transfers `tokenId` from `from` to `to`. 459 | * 460 | * Requirements: 461 | * 462 | * - `to` cannot be the zero address. 463 | * - `tokenId` token must be owned by `from`. 464 | * 465 | * Emits a {Transfer} event. 466 | */ 467 | function _transfer( 468 | address from, 469 | address to, 470 | uint256 tokenId 471 | ) private { 472 | TokenOwnership memory prevOwnership = _ownershipOf(tokenId); 473 | 474 | if (prevOwnership.addr != from) revert TransferFromIncorrectOwner(); 475 | 476 | bool isApprovedOrOwner = (_msgSender() == from || 477 | isApprovedForAll(from, _msgSender()) || 478 | getApproved(tokenId) == _msgSender()); 479 | 480 | if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); 481 | if (to == address(0)) revert TransferToZeroAddress(); 482 | 483 | _beforeTokenTransfers(from, to, tokenId, 1); 484 | 485 | // Clear approvals from the previous owner 486 | _approve(address(0), tokenId, from); 487 | 488 | // Underflow of the sender's balance is impossible because we check for 489 | // ownership above and the recipient's balance can't realistically overflow. 490 | // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. 491 | unchecked { 492 | _addressData[from].balance -= 1; 493 | _addressData[to].balance += 1; 494 | 495 | TokenOwnership storage currSlot = _ownerships[tokenId]; 496 | currSlot.addr = to; 497 | currSlot.startTimestamp = uint64(block.timestamp); 498 | 499 | // If the ownership slot of tokenId+1 is not explicitly set, that means the transfer initiator owns it. 500 | // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. 501 | uint256 nextTokenId = tokenId + 1; 502 | TokenOwnership storage nextSlot = _ownerships[nextTokenId]; 503 | if (nextSlot.addr == address(0)) { 504 | // This will suffice for checking _exists(nextTokenId), 505 | // as a burned slot cannot contain the zero address. 506 | if (nextTokenId != _currentIndex) { 507 | nextSlot.addr = from; 508 | nextSlot.startTimestamp = prevOwnership.startTimestamp; 509 | } 510 | } 511 | } 512 | 513 | emit Transfer(from, to, tokenId); 514 | _afterTokenTransfers(from, to, tokenId, 1); 515 | } 516 | 517 | /** 518 | * @dev This is equivalent to _burn(tokenId, false) 519 | */ 520 | function _burn(uint256 tokenId) internal virtual { 521 | _burn(tokenId, false); 522 | } 523 | 524 | /** 525 | * @dev Destroys `tokenId`. 526 | * The approval is cleared when the token is burned. 527 | * 528 | * Requirements: 529 | * 530 | * - `tokenId` must exist. 531 | * 532 | * Emits a {Transfer} event. 533 | */ 534 | function _burn(uint256 tokenId, bool approvalCheck) internal virtual { 535 | TokenOwnership memory prevOwnership = _ownershipOf(tokenId); 536 | 537 | address from = prevOwnership.addr; 538 | 539 | if (approvalCheck) { 540 | bool isApprovedOrOwner = (_msgSender() == from || 541 | isApprovedForAll(from, _msgSender()) || 542 | getApproved(tokenId) == _msgSender()); 543 | 544 | if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); 545 | } 546 | 547 | _beforeTokenTransfers(from, address(0), tokenId, 1); 548 | 549 | // Clear approvals from the previous owner 550 | _approve(address(0), tokenId, from); 551 | 552 | // Underflow of the sender's balance is impossible because we check for 553 | // ownership above and the recipient's balance can't realistically overflow. 554 | // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. 555 | unchecked { 556 | AddressData storage addressData = _addressData[from]; 557 | addressData.balance -= 1; 558 | addressData.numberBurned += 1; 559 | 560 | // Keep track of who burned the token, and the timestamp of burning. 561 | TokenOwnership storage currSlot = _ownerships[tokenId]; 562 | currSlot.addr = from; 563 | currSlot.startTimestamp = uint64(block.timestamp); 564 | currSlot.burned = true; 565 | 566 | // If the ownership slot of tokenId+1 is not explicitly set, that means the burn initiator owns it. 567 | // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. 568 | uint256 nextTokenId = tokenId + 1; 569 | TokenOwnership storage nextSlot = _ownerships[nextTokenId]; 570 | if (nextSlot.addr == address(0)) { 571 | // This will suffice for checking _exists(nextTokenId), 572 | // as a burned slot cannot contain the zero address. 573 | if (nextTokenId != _currentIndex) { 574 | nextSlot.addr = from; 575 | nextSlot.startTimestamp = prevOwnership.startTimestamp; 576 | } 577 | } 578 | } 579 | 580 | emit Transfer(from, address(0), tokenId); 581 | _afterTokenTransfers(from, address(0), tokenId, 1); 582 | 583 | // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. 584 | unchecked { 585 | _burnCounter++; 586 | } 587 | } 588 | 589 | /** 590 | * @dev Approve `to` to operate on `tokenId` 591 | * 592 | * Emits a {Approval} event. 593 | */ 594 | function _approve( 595 | address to, 596 | uint256 tokenId, 597 | address owner 598 | ) private { 599 | _tokenApprovals[tokenId] = to; 600 | emit Approval(owner, to, tokenId); 601 | } 602 | 603 | /** 604 | * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target contract. 605 | * 606 | * @param from address representing the previous owner of the given token ID 607 | * @param to target address that will receive the tokens 608 | * @param tokenId uint256 ID of the token to be transferred 609 | * @param _data bytes optional data to send along with the call 610 | * @return bool whether the call correctly returned the expected magic value 611 | */ 612 | function _checkContractOnERC721Received( 613 | address from, 614 | address to, 615 | uint256 tokenId, 616 | bytes memory _data 617 | ) private returns (bool) { 618 | try 619 | IERC721Receiver(to).onERC721Received( 620 | _msgSender(), 621 | from, 622 | tokenId, 623 | _data 624 | ) 625 | returns (bytes4 retval) { 626 | return retval == IERC721Receiver(to).onERC721Received.selector; 627 | } catch (bytes memory reason) { 628 | if (reason.length == 0) { 629 | revert TransferToNonERC721ReceiverImplementer(); 630 | } else { 631 | assembly { 632 | revert(add(32, reason), mload(reason)) 633 | } 634 | } 635 | } 636 | } 637 | 638 | /** 639 | * @dev Hook that is called before a set of serially-ordered token ids are about to be transferred. This includes minting. 640 | * And also called before burning one token. 641 | * 642 | * startTokenId - the first token id to be transferred 643 | * quantity - the amount to be transferred 644 | * 645 | * Calling conditions: 646 | * 647 | * - When `from` and `to` are both non-zero, `from`'s `tokenId` will be 648 | * transferred to `to`. 649 | * - When `from` is zero, `tokenId` will be minted for `to`. 650 | * - When `to` is zero, `tokenId` will be burned by `from`. 651 | * - `from` and `to` are never both zero. 652 | */ 653 | function _beforeTokenTransfers( 654 | address from, 655 | address to, 656 | uint256 startTokenId, 657 | uint256 quantity 658 | ) internal virtual {} 659 | 660 | /** 661 | * @dev Hook that is called after a set of serially-ordered token ids have been transferred. This includes 662 | * minting. 663 | * And also called after one token has been burned. 664 | * 665 | * startTokenId - the first token id to be transferred 666 | * quantity - the amount to be transferred 667 | * 668 | * Calling conditions: 669 | * 670 | * - When `from` and `to` are both non-zero, `from`'s `tokenId` has been 671 | * transferred to `to`. 672 | * - When `from` is zero, `tokenId` has been minted for `to`. 673 | * - When `to` is zero, `tokenId` has been burned by `from`. 674 | * - `from` and `to` are never both zero. 675 | */ 676 | function _afterTokenTransfers( 677 | address from, 678 | address to, 679 | uint256 startTokenId, 680 | uint256 quantity 681 | ) internal virtual {} 682 | } 683 | -------------------------------------------------------------------------------- /contracts/GalleryToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 7 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 8 | import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; 9 | import "@openzeppelin/contracts/access/Ownable.sol"; 10 | 11 | contract GalleryToken is ERC20, Ownable { 12 | enum TokenType { 13 | ERC721, 14 | ERC1155 15 | } 16 | struct ERC721Balance { 17 | TokenType tokenType; 18 | address tokenContract; 19 | uint256 balance; 20 | uint256 timeStaked; 21 | } 22 | 23 | uint256 private constant PERCENTAGE_GALLERY = 100; 24 | uint256 private constant PERCENTAGE_STAKEE = 100; 25 | 26 | // mapping of staker to list of erc721 balances for tokens at time of stake 27 | mapping(address => ERC721Balance[]) internal _erc721BalancesAtStake; 28 | 29 | mapping(address => mapping(address => uint256)) internal stakes; 30 | 31 | constructor(string memory name_, string memory symbol_) 32 | ERC20(name_, symbol_) 33 | {} 34 | 35 | function createStake( 36 | uint256 _stake, 37 | address _stakee, 38 | address[] calldata _contracts 39 | ) public { 40 | require(_stake > 0, "stake must be greater than 0"); 41 | require(_stakee != address(0), "stakee must not be the zero address"); 42 | for (uint256 i = 0; i < _contracts.length; i++) { 43 | require( 44 | ERC165Checker.supportsInterface( 45 | _contracts[i], 46 | type(IERC721).interfaceId 47 | ), 48 | "contract must support ERC721" 49 | ); 50 | IERC721 token = IERC721(_contracts[i]); 51 | uint256 balance = token.balanceOf(_stakee); 52 | require(balance > 0, "stakee must have balance"); 53 | _erc721BalancesAtStake[_stakee].push( 54 | ERC721Balance( 55 | TokenType.ERC721, 56 | _contracts[i], 57 | balance, 58 | block.timestamp 59 | ) 60 | ); 61 | } 62 | _burn(_msgSender(), _stake); 63 | stakes[_msgSender()][_stakee] += _stake; 64 | } 65 | 66 | function removeStake(address _stakee) public { 67 | uint256 currentStake = stakes[_msgSender()][_stakee]; 68 | for (uint256 i = 0; i < _erc721BalancesAtStake[_stakee].length; i++) { 69 | ERC721Balance memory tokenBalance = _erc721BalancesAtStake[_stakee][ 70 | i 71 | ]; 72 | uint256 balanceAtStart = tokenBalance.balance; 73 | _erc721BalancesAtStake[_stakee][i] = _erc721BalancesAtStake[ 74 | _stakee 75 | ][_erc721BalancesAtStake[_stakee].length - 1]; 76 | _erc721BalancesAtStake[_stakee].pop(); 77 | IERC721 token = IERC721(tokenBalance.tokenContract); 78 | uint256 balance = token.balanceOf(_stakee); 79 | if (balanceAtStart >= balance) { 80 | currentStake += 81 | currentStake / 82 | (((block.timestamp / tokenBalance.timeStaked) * 83 | (balanceAtStart / balance)) * 12); 84 | } else { 85 | currentStake -= 86 | currentStake / 87 | (((block.timestamp / tokenBalance.timeStaked) * 88 | (balance / balanceAtStart)) * 12); 89 | } 90 | } 91 | uint256 fees = (currentStake / PERCENTAGE_STAKEE) + 92 | (currentStake / PERCENTAGE_GALLERY); 93 | _mint(_msgSender(), currentStake - fees); 94 | _mint(owner(), (currentStake / PERCENTAGE_GALLERY)); 95 | _mint(_stakee, (currentStake / PERCENTAGE_STAKEE)); 96 | stakes[_msgSender()][_stakee] = 0; 97 | } 98 | 99 | function stakeOf(address _stakeholder, address _stakee) 100 | public 101 | view 102 | returns (uint256) 103 | { 104 | return stakes[_stakeholder][_stakee]; 105 | } 106 | 107 | function isStakeholder(address _address, address _stakee) 108 | public 109 | view 110 | returns (bool) 111 | { 112 | return stakes[_address][_stakee] > 0; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /contracts/IMarketplace.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IMarketplace { 6 | event Sale( 7 | uint256 indexed id, 8 | address indexed seller, 9 | address indexed buyer, 10 | uint256 price 11 | ); 12 | event AuctionStart( 13 | uint256 indexed id, 14 | address indexed seller, 15 | uint256 startingBid 16 | ); 17 | 18 | struct Offer { 19 | uint256 value; 20 | uint256 amount; 21 | address bidder; 22 | } 23 | struct Listing { 24 | uint256 price; 25 | uint256 amount; 26 | address seller; 27 | } 28 | 29 | struct Auction { 30 | uint256 end; 31 | uint256 amount; 32 | address seller; 33 | uint256 startingBid; 34 | } 35 | 36 | function makeOffer( 37 | uint256 value, 38 | uint256 amount, 39 | uint256 tokenID, 40 | address tokenContract 41 | ) external; 42 | 43 | function recallOffer(uint256 tokenID, address tokenContract) external; 44 | 45 | function acceptCurrentOffer(uint256 tokenID, address tokenContract) 46 | external; 47 | 48 | function listToken( 49 | uint256 tokenID, 50 | address tokenContract, 51 | uint256 price, 52 | uint256 amount 53 | ) external; 54 | 55 | function purchaseToken(uint256 tokenID, address tokenContract) 56 | external 57 | payable; 58 | 59 | function startAuction( 60 | uint256 tokenID, 61 | address tokenContract, 62 | uint256 end, 63 | uint256 startingBid, 64 | uint256 amount 65 | ) external; 66 | 67 | function endAuction(uint256 tokenID, address tokenContract) external; 68 | } 69 | -------------------------------------------------------------------------------- /contracts/Marketplace.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./IMarketplace.sol"; 6 | import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 9 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 10 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 11 | import "@openzeppelin/contracts/utils/Address.sol"; 12 | 13 | contract Marketplace is IMarketplace, ReentrancyGuard { 14 | using Address for address payable; 15 | 16 | mapping(address => mapping(uint256 => Listing)) private listings; 17 | mapping(address => mapping(uint256 => Auction)) private auctions; 18 | mapping(address => mapping(uint256 => Offer[])) private offers; 19 | 20 | IERC20 private constant wethContract = 21 | IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 22 | 23 | uint256 private constant minAuctionTime = 1 hours; 24 | 25 | function makeOffer( 26 | uint256 value, 27 | uint256 amount, 28 | uint256 tokenID, 29 | address tokenContract 30 | ) external override nonReentrant { 31 | require( 32 | value > 0 && amount > 0, 33 | "Marketplace: invalid offer amount or price" 34 | ); 35 | require( 36 | wethContract.allowance(msg.sender, address(this)) > amount, 37 | "Marketplace: Not enough WETH approved for offer" 38 | ); 39 | uint256 len = offers[tokenContract][tokenID].length; 40 | if (len > 0) { 41 | require( 42 | offers[tokenContract][tokenID][len - 1].value < value, 43 | "Marketplace: Cannot place lower offer than previous offer" 44 | ); 45 | } 46 | Auction memory auction = auctions[tokenContract][tokenID]; 47 | require( 48 | auction.amount == 0 || auction.amount == amount, 49 | "Marketplace: wrong amount for purchase at auction" 50 | ); 51 | require( 52 | auction.end == 0 || auction.end > block.timestamp, 53 | "Marketplace: auction ended" 54 | ); 55 | require( 56 | auction.startingBid == 0 || auction.startingBid < value, 57 | "Marketplace: bid too low for auction" 58 | ); 59 | if (amount == listings[tokenContract][tokenID].amount) { 60 | require( 61 | listings[tokenContract][tokenID].price > value, 62 | "Marketplace: purchase instead of offer" 63 | ); 64 | } 65 | offers[tokenContract][tokenID].push(Offer(value, amount, msg.sender)); 66 | } 67 | 68 | function recallOffer(uint256 tokenID, address tokenContract) 69 | external 70 | override 71 | { 72 | require( 73 | auctions[tokenContract][tokenID].end == 0 || 74 | auctions[tokenContract][tokenID].end > block.timestamp, 75 | "Marketplace: auction ended" 76 | ); 77 | for (uint256 i = 0; i < offers[tokenContract][tokenID].length; i++) { 78 | if (offers[tokenContract][tokenID][i].bidder == msg.sender) { 79 | delete offers[tokenContract][tokenID][i]; 80 | } 81 | } 82 | } 83 | 84 | function acceptCurrentOffer(uint256 tokenID, address tokenContract) 85 | external 86 | override 87 | nonReentrant 88 | { 89 | require( 90 | auctions[tokenContract][tokenID].end == 0, 91 | "Marketplace: auction in progress" 92 | ); 93 | 94 | uint256 len = offers[tokenContract][tokenID].length; 95 | require(len > 0, "Marketplace: no offers"); 96 | Offer memory curOffer = offers[tokenContract][tokenID][len - 1]; 97 | require(curOffer.amount > 0 && curOffer.bidder != address(0)); 98 | transferToken( 99 | tokenID, 100 | tokenContract, 101 | curOffer.amount, 102 | msg.sender, 103 | curOffer.bidder 104 | ); 105 | 106 | wethContract.transferFrom(curOffer.bidder, msg.sender, curOffer.value); 107 | } 108 | 109 | function listToken( 110 | uint256 tokenID, 111 | address tokenContract, 112 | uint256 price, 113 | uint256 amount 114 | ) external override { 115 | require( 116 | auctions[tokenContract][tokenID].end == 0, 117 | "Marketplace: auction in progress" 118 | ); 119 | 120 | requireControlToken(tokenID, tokenContract, amount); 121 | 122 | listings[tokenContract][tokenID].price = price; 123 | listings[tokenContract][tokenID].amount = amount; 124 | } 125 | 126 | function purchaseToken(uint256 tokenID, address tokenContract) 127 | external 128 | payable 129 | override 130 | nonReentrant 131 | { 132 | Listing memory listing = listings[tokenContract][tokenID]; 133 | require(listing.amount > 0, "Marketplace: no listing"); 134 | require( 135 | msg.value == listings[tokenContract][tokenID].price, 136 | "Marketplace: wrong price" 137 | ); 138 | address payable seller = payable( 139 | listings[tokenContract][tokenID].seller 140 | ); 141 | 142 | transferToken( 143 | tokenID, 144 | tokenContract, 145 | listing.amount, 146 | listing.seller, 147 | msg.sender 148 | ); 149 | 150 | seller.sendValue(msg.value); 151 | } 152 | 153 | function startAuction( 154 | uint256 tokenID, 155 | address tokenContract, 156 | uint256 end, 157 | uint256 startingBid, 158 | uint256 amount 159 | ) external override { 160 | require( 161 | listings[tokenContract][tokenID].amount == 0, 162 | "Marketplace: listed already" 163 | ); 164 | require( 165 | end - minAuctionTime > block.timestamp, 166 | "Marketplace: auction too short" 167 | ); 168 | requireControlToken(tokenID, tokenContract, amount); 169 | auctions[tokenContract][tokenID] = Auction( 170 | end, 171 | amount, 172 | msg.sender, 173 | startingBid 174 | ); 175 | } 176 | 177 | function endAuction(uint256 tokenID, address tokenContract) 178 | external 179 | override 180 | nonReentrant 181 | { 182 | Auction memory auction = auctions[tokenContract][tokenID]; 183 | require(auction.end < block.timestamp, "Marketplace: auction ended"); 184 | uint256 len = offers[tokenContract][tokenID].length; 185 | require(len > 0, "Marketplace: no offers"); 186 | Offer memory curOffer = offers[tokenContract][tokenID][len - 1]; 187 | transferToken( 188 | tokenID, 189 | tokenContract, 190 | auction.amount, 191 | auction.seller, 192 | curOffer.bidder 193 | ); 194 | wethContract.transferFrom( 195 | curOffer.bidder, 196 | auction.seller, 197 | curOffer.value 198 | ); 199 | } 200 | 201 | function requireControlToken( 202 | uint256 tokenID, 203 | address tokenContract, 204 | uint256 amount 205 | ) private view { 206 | IERC165 erc165 = IERC165(tokenContract); 207 | 208 | if (erc165.supportsInterface(type(IERC721).interfaceId)) { 209 | require(amount == 1, "Marketplace: can only list one of an NFT"); 210 | IERC721 token = IERC721(tokenContract); 211 | require( 212 | token.ownerOf(tokenID) == msg.sender, 213 | "Marketplace: not owner" 214 | ); 215 | require( 216 | token.isApprovedForAll(msg.sender, address(this)), 217 | "Marketplace: not approved" 218 | ); 219 | } else if (erc165.supportsInterface(type(IERC1155).interfaceId)) { 220 | IERC1155 token = IERC1155(tokenContract); 221 | require( 222 | token.balanceOf(msg.sender, tokenID) >= amount, 223 | "Marketplace: not enough tokens" 224 | ); 225 | require( 226 | token.isApprovedForAll(msg.sender, address(this)), 227 | "Marketplace: not approved" 228 | ); 229 | } else { 230 | revert("Marketplace: unsupported token type"); 231 | } 232 | } 233 | 234 | function transferToken( 235 | uint256 tokenID, 236 | address tokenContract, 237 | uint256 amount, 238 | address from, 239 | address to 240 | ) private { 241 | IERC165 erc165 = IERC165(tokenContract); 242 | 243 | if (erc165.supportsInterface(type(IERC721).interfaceId)) { 244 | IERC721 token = IERC721(tokenContract); 245 | token.safeTransferFrom(from, to, tokenID); 246 | } else if (erc165.supportsInterface(type(IERC1155).interfaceId)) { 247 | IERC1155 token = IERC1155(tokenContract); 248 | token.safeTransferFrom( 249 | from, 250 | to, 251 | tokenID, 252 | amount, 253 | abi.encodePacked("") 254 | ); 255 | } else { 256 | revert("Marketplace: unsupported token type"); 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /contracts/Series.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | contract Series is ERC721, Ownable { 9 | mapping(uint256 => string) internal _tokenURIs; 10 | 11 | constructor( 12 | string memory name, 13 | string memory symbol 14 | ) ERC721(name, symbol) {} 15 | 16 | function mint( 17 | address to, 18 | uint256 tokenId, 19 | string memory uri 20 | ) public virtual { 21 | _mint(to, tokenId); 22 | _tokenURIs[tokenId] = uri; 23 | } 24 | 25 | function setURI( 26 | uint256 tokenId, 27 | string memory uri 28 | ) public virtual onlyOwner { 29 | require( 30 | _exists(tokenId), 31 | "ERC721Metadata: URI set of nonexistent token" 32 | ); 33 | _tokenURIs[tokenId] = uri; 34 | } 35 | 36 | function tokenURI( 37 | uint256 tokenId 38 | ) public view virtual override returns (string memory) { 39 | require( 40 | _exists(tokenId), 41 | "ERC721Metadata: URI query for nonexistent token" 42 | ); 43 | 44 | return _tokenURIs[tokenId]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /contracts/TestNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | 7 | contract TestNFT is ERC721 { 8 | constructor(string memory name, string memory symbol) 9 | ERC721(name, symbol) 10 | {} 11 | 12 | function mint(address to, uint256 tokenId) public virtual { 13 | _mint(to, tokenId); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/cards/ERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; 7 | import "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; 8 | import "@openzeppelin/contracts/utils/Address.sol"; 9 | import "@openzeppelin/contracts/utils/Context.sol"; 10 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; 11 | 12 | /** 13 | * @dev Implementation of the basic standard multi-token. 14 | * See https://eips.ethereum.org/EIPS/eip-1155 15 | * Originally based on code by Enjin: https://github.com/enjin/erc-1155 16 | * 17 | * _Available since v3.1._ 18 | */ 19 | contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { 20 | using Address for address; 21 | 22 | // Mapping from token ID to account balances 23 | mapping(uint256 => mapping(address => uint256)) internal balances; 24 | 25 | // Mapping from account to operator approvals 26 | mapping(address => mapping(address => bool)) internal operatorApprovals; 27 | 28 | // Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json 29 | string private _uri; 30 | 31 | /** 32 | * @dev See {_setURI}. 33 | */ 34 | constructor(string memory uri_) { 35 | _setURI(uri_); 36 | } 37 | 38 | /** 39 | * @dev See {IERC165-supportsInterface}. 40 | */ 41 | function supportsInterface(bytes4 interfaceId) 42 | public 43 | view 44 | virtual 45 | override(ERC165, IERC165) 46 | returns (bool) 47 | { 48 | return 49 | interfaceId == type(IERC1155).interfaceId || 50 | interfaceId == type(IERC1155MetadataURI).interfaceId || 51 | super.supportsInterface(interfaceId); 52 | } 53 | 54 | /** 55 | * @dev See {IERC1155MetadataURI-uri}. 56 | * 57 | * This implementation returns the same URI for *all* token types. It relies 58 | * on the token type ID substitution mechanism 59 | * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. 60 | * 61 | * Clients calling this function must replace the `\{id\}` substring with the 62 | * actual token type ID. 63 | */ 64 | function uri(uint256) public view virtual override returns (string memory) { 65 | return _uri; 66 | } 67 | 68 | /** 69 | * @dev See {IERC1155-balanceOf}. 70 | * 71 | * Requirements: 72 | * 73 | * - `account` cannot be the zero address. 74 | */ 75 | function balanceOf(address account, uint256 id) 76 | public 77 | view 78 | virtual 79 | override 80 | returns (uint256) 81 | { 82 | require( 83 | account != address(0), 84 | "ERC1155: balance query for the zero address" 85 | ); 86 | return balances[id][account]; 87 | } 88 | 89 | /** 90 | * @dev See {IERC1155-balanceOfBatch}. 91 | * 92 | * Requirements: 93 | * 94 | * - `accounts` and `ids` must have the same length. 95 | */ 96 | function balanceOfBatch(address[] memory accounts, uint256[] memory ids) 97 | public 98 | view 99 | virtual 100 | override 101 | returns (uint256[] memory) 102 | { 103 | require( 104 | accounts.length == ids.length, 105 | "ERC1155: accounts and ids length mismatch" 106 | ); 107 | 108 | uint256[] memory batchBalances = new uint256[](accounts.length); 109 | 110 | for (uint256 i = 0; i < accounts.length; ++i) { 111 | batchBalances[i] = balanceOf(accounts[i], ids[i]); 112 | } 113 | 114 | return batchBalances; 115 | } 116 | 117 | /** 118 | * @dev See {IERC1155-setApprovalForAll}. 119 | */ 120 | function setApprovalForAll(address operator, bool approved) 121 | public 122 | virtual 123 | override 124 | { 125 | require( 126 | _msgSender() != operator, 127 | "ERC1155: setting approval status for self" 128 | ); 129 | 130 | operatorApprovals[_msgSender()][operator] = approved; 131 | emit ApprovalForAll(_msgSender(), operator, approved); 132 | } 133 | 134 | /** 135 | * @dev See {IERC1155-isApprovedForAll}. 136 | */ 137 | function isApprovedForAll(address account, address operator) 138 | public 139 | view 140 | virtual 141 | override 142 | returns (bool) 143 | { 144 | return operatorApprovals[account][operator]; 145 | } 146 | 147 | /** 148 | * @dev See {IERC1155-safeTransferFrom}. 149 | */ 150 | function safeTransferFrom( 151 | address from, 152 | address to, 153 | uint256 id, 154 | uint256 amount, 155 | bytes memory data 156 | ) public virtual override { 157 | require( 158 | from == _msgSender() || isApprovedForAll(from, _msgSender()), 159 | "ERC1155: caller is not owner nor approved" 160 | ); 161 | _safeTransferFrom(from, to, id, amount, data); 162 | } 163 | 164 | /** 165 | * @dev See {IERC1155-safeBatchTransferFrom}. 166 | */ 167 | function safeBatchTransferFrom( 168 | address from, 169 | address to, 170 | uint256[] memory ids, 171 | uint256[] memory amounts, 172 | bytes memory data 173 | ) public virtual override { 174 | require( 175 | from == _msgSender() || isApprovedForAll(from, _msgSender()), 176 | "ERC1155: transfer caller is not owner nor approved" 177 | ); 178 | _safeBatchTransferFrom(from, to, ids, amounts, data); 179 | } 180 | 181 | /** 182 | * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. 183 | * 184 | * Emits a {TransferSingle} event. 185 | * 186 | * Requirements: 187 | * 188 | * - `to` cannot be the zero address. 189 | * - `from` must have a balance of tokens of type `id` of at least `amount`. 190 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the 191 | * acceptance magic value. 192 | */ 193 | function _safeTransferFrom( 194 | address from, 195 | address to, 196 | uint256 id, 197 | uint256 amount, 198 | bytes memory data 199 | ) internal virtual { 200 | require(to != address(0), "ERC1155: transfer to the zero address"); 201 | 202 | address operator = _msgSender(); 203 | 204 | _beforeTokenTransfer( 205 | operator, 206 | from, 207 | to, 208 | _asSingletonArray(id), 209 | _asSingletonArray(amount), 210 | data 211 | ); 212 | 213 | uint256 fromBalance = balances[id][from]; 214 | require( 215 | fromBalance >= amount, 216 | "ERC1155: insufficient balance for transfer" 217 | ); 218 | unchecked { 219 | balances[id][from] = fromBalance - amount; 220 | } 221 | 222 | balances[id][to] += amount; 223 | 224 | emit TransferSingle(operator, from, to, id, amount); 225 | 226 | _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); 227 | } 228 | 229 | /** 230 | * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_safeTransferFrom}. 231 | * 232 | * Emits a {TransferBatch} event. 233 | * 234 | * Requirements: 235 | * 236 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the 237 | * acceptance magic value. 238 | */ 239 | function _safeBatchTransferFrom( 240 | address from, 241 | address to, 242 | uint256[] memory ids, 243 | uint256[] memory amounts, 244 | bytes memory data 245 | ) internal virtual { 246 | require( 247 | ids.length == amounts.length, 248 | "ERC1155: ids and amounts length mismatch" 249 | ); 250 | require(to != address(0), "ERC1155: transfer to the zero address"); 251 | 252 | address operator = _msgSender(); 253 | 254 | _beforeTokenTransfer(operator, from, to, ids, amounts, data); 255 | 256 | for (uint256 i = 0; i < ids.length; ++i) { 257 | uint256 id = ids[i]; 258 | uint256 amount = amounts[i]; 259 | 260 | uint256 fromBalance = balances[id][from]; 261 | require( 262 | fromBalance >= amount, 263 | "ERC1155: insufficient balance for transfer" 264 | ); 265 | unchecked { 266 | balances[id][from] = fromBalance - amount; 267 | } 268 | 269 | balances[id][to] += amount; 270 | } 271 | 272 | emit TransferBatch(operator, from, to, ids, amounts); 273 | 274 | _doSafeBatchTransferAcceptanceCheck( 275 | operator, 276 | from, 277 | to, 278 | ids, 279 | amounts, 280 | data 281 | ); 282 | } 283 | 284 | /** 285 | * @dev Sets a new URI for all token types, by relying on the token type ID 286 | * substitution mechanism 287 | * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. 288 | * 289 | * By this mechanism, any occurrence of the `\{id\}` substring in either the 290 | * URI or any of the amounts in the JSON file at said URI will be replaced by 291 | * clients with the token type ID. 292 | * 293 | * For example, the `https://token-cdn-domain/\{id\}.json` URI would be 294 | * interpreted by clients as 295 | * `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` 296 | * for token type ID 0x4cce0. 297 | * 298 | * See {uri}. 299 | * 300 | * Because these URIs cannot be meaningfully represented by the {URI} event, 301 | * this function emits no events. 302 | */ 303 | function _setURI(string memory newuri) internal virtual { 304 | _uri = newuri; 305 | } 306 | 307 | /** 308 | * @dev Creates `amount` tokens of token type `id`, and assigns them to `account`. 309 | * 310 | * Emits a {TransferSingle} event. 311 | * 312 | * Requirements: 313 | * 314 | * - `account` cannot be the zero address. 315 | * - If `account` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the 316 | * acceptance magic value. 317 | */ 318 | function _mint( 319 | address account, 320 | uint256 id, 321 | uint256 amount, 322 | bytes memory data 323 | ) internal virtual { 324 | require(account != address(0), "ERC1155: mint to the zero address"); 325 | 326 | address operator = _msgSender(); 327 | 328 | _beforeTokenTransfer( 329 | operator, 330 | address(0), 331 | account, 332 | _asSingletonArray(id), 333 | _asSingletonArray(amount), 334 | data 335 | ); 336 | 337 | balances[id][account] += amount; 338 | emit TransferSingle(operator, address(0), account, id, amount); 339 | 340 | _doSafeTransferAcceptanceCheck( 341 | operator, 342 | address(0), 343 | account, 344 | id, 345 | amount, 346 | data 347 | ); 348 | } 349 | 350 | /** 351 | * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_mint}. 352 | * 353 | * Requirements: 354 | * 355 | * - `ids` and `amounts` must have the same length. 356 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the 357 | * acceptance magic value. 358 | */ 359 | function _mintBatch( 360 | address to, 361 | uint256[] memory ids, 362 | uint256[] memory amounts, 363 | bytes memory data 364 | ) internal virtual { 365 | require(to != address(0), "ERC1155: mint to the zero address"); 366 | require( 367 | ids.length == amounts.length, 368 | "ERC1155: ids and amounts length mismatch" 369 | ); 370 | 371 | address operator = _msgSender(); 372 | 373 | _beforeTokenTransfer(operator, address(0), to, ids, amounts, data); 374 | 375 | for (uint256 i = 0; i < ids.length; i++) { 376 | balances[ids[i]][to] += amounts[i]; 377 | } 378 | 379 | emit TransferBatch(operator, address(0), to, ids, amounts); 380 | 381 | _doSafeBatchTransferAcceptanceCheck( 382 | operator, 383 | address(0), 384 | to, 385 | ids, 386 | amounts, 387 | data 388 | ); 389 | } 390 | 391 | /** 392 | * @dev Destroys `amount` tokens of token type `id` from `account` 393 | * 394 | * Requirements: 395 | * 396 | * - `account` cannot be the zero address. 397 | * - `account` must have at least `amount` tokens of token type `id`. 398 | */ 399 | function _burn( 400 | address account, 401 | uint256 id, 402 | uint256 amount 403 | ) internal virtual { 404 | require(account != address(0), "ERC1155: burn from the zero address"); 405 | 406 | address operator = _msgSender(); 407 | 408 | _beforeTokenTransfer( 409 | operator, 410 | account, 411 | address(0), 412 | _asSingletonArray(id), 413 | _asSingletonArray(amount), 414 | "" 415 | ); 416 | 417 | uint256 accountBalance = balances[id][account]; 418 | require( 419 | accountBalance >= amount, 420 | "ERC1155: burn amount exceeds balance" 421 | ); 422 | unchecked { 423 | balances[id][account] = accountBalance - amount; 424 | } 425 | 426 | emit TransferSingle(operator, account, address(0), id, amount); 427 | } 428 | 429 | /** 430 | * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_burn}. 431 | * 432 | * Requirements: 433 | * 434 | * - `ids` and `amounts` must have the same length. 435 | */ 436 | function _burnBatch( 437 | address account, 438 | uint256[] memory ids, 439 | uint256[] memory amounts 440 | ) internal virtual { 441 | require(account != address(0), "ERC1155: burn from the zero address"); 442 | require( 443 | ids.length == amounts.length, 444 | "ERC1155: ids and amounts length mismatch" 445 | ); 446 | 447 | address operator = _msgSender(); 448 | 449 | _beforeTokenTransfer(operator, account, address(0), ids, amounts, ""); 450 | 451 | for (uint256 i = 0; i < ids.length; i++) { 452 | uint256 id = ids[i]; 453 | uint256 amount = amounts[i]; 454 | 455 | uint256 accountBalance = balances[id][account]; 456 | require( 457 | accountBalance >= amount, 458 | "ERC1155: burn amount exceeds balance" 459 | ); 460 | unchecked { 461 | balances[id][account] = accountBalance - amount; 462 | } 463 | } 464 | 465 | emit TransferBatch(operator, account, address(0), ids, amounts); 466 | } 467 | 468 | /** 469 | * @dev Hook that is called before any token transfer. This includes minting 470 | * and burning, as well as batched variants. 471 | * 472 | * The same hook is called on both single and batched variants. For single 473 | * transfers, the length of the `id` and `amount` arrays will be 1. 474 | * 475 | * Calling conditions (for each `id` and `amount` pair): 476 | * 477 | * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens 478 | * of token type `id` will be transferred to `to`. 479 | * - When `from` is zero, `amount` tokens of token type `id` will be minted 480 | * for `to`. 481 | * - when `to` is zero, `amount` of ``from``'s tokens of token type `id` 482 | * will be burned. 483 | * - `from` and `to` are never both zero. 484 | * - `ids` and `amounts` have the same, non-zero length. 485 | * 486 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 487 | */ 488 | function _beforeTokenTransfer( 489 | address operator, 490 | address from, 491 | address to, 492 | uint256[] memory ids, 493 | uint256[] memory amounts, 494 | bytes memory data 495 | ) internal virtual {} 496 | 497 | function _doSafeTransferAcceptanceCheck( 498 | address operator, 499 | address from, 500 | address to, 501 | uint256 id, 502 | uint256 amount, 503 | bytes memory data 504 | ) internal { 505 | if (to.isContract()) { 506 | try 507 | IERC1155Receiver(to).onERC1155Received( 508 | operator, 509 | from, 510 | id, 511 | amount, 512 | data 513 | ) 514 | returns (bytes4 response) { 515 | if (response != IERC1155Receiver.onERC1155Received.selector) { 516 | revert("ERC1155: ERC1155Receiver rejected tokens"); 517 | } 518 | } catch Error(string memory reason) { 519 | revert(reason); 520 | } catch { 521 | revert("ERC1155: transfer to non ERC1155Receiver implementer"); 522 | } 523 | } 524 | } 525 | 526 | function _doSafeBatchTransferAcceptanceCheck( 527 | address operator, 528 | address from, 529 | address to, 530 | uint256[] memory ids, 531 | uint256[] memory amounts, 532 | bytes memory data 533 | ) internal { 534 | if (to.isContract()) { 535 | try 536 | IERC1155Receiver(to).onERC1155BatchReceived( 537 | operator, 538 | from, 539 | ids, 540 | amounts, 541 | data 542 | ) 543 | returns (bytes4 response) { 544 | if ( 545 | response != IERC1155Receiver.onERC1155BatchReceived.selector 546 | ) { 547 | revert("ERC1155: ERC1155Receiver rejected tokens"); 548 | } 549 | } catch Error(string memory reason) { 550 | revert(reason); 551 | } catch { 552 | revert("ERC1155: transfer to non ERC1155Receiver implementer"); 553 | } 554 | } 555 | } 556 | 557 | function _asSingletonArray(uint256 element) 558 | internal 559 | pure 560 | returns (uint256[] memory) 561 | { 562 | uint256[] memory array = new uint256[](1); 563 | array[0] = element; 564 | 565 | return array; 566 | } 567 | } 568 | -------------------------------------------------------------------------------- /contracts/cards/GeneralCards.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | import "@openzeppelin/contracts/access/Ownable.sol"; 10 | import "@openzeppelin/contracts/utils/Address.sol"; 11 | import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; 12 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 13 | 14 | contract GeneralCards is ERC1155, Ownable, ReentrancyGuard { 15 | using Address for address payable; 16 | 17 | struct TokenType { 18 | uint256 price; 19 | uint256 usedSupply; 20 | uint256 totalSupply; 21 | bytes32 allowListMerkleRoot; 22 | string uri; 23 | } 24 | 25 | bool private canMint; 26 | 27 | mapping(uint256 => TokenType) _tokenTypes; 28 | mapping(address => mapping(uint256 => bool)) private _hasMinted; 29 | 30 | string public constant name = "GeneralCards"; 31 | string public constant symbol = "GMC"; 32 | 33 | constructor() ERC1155("") {} 34 | 35 | function createType( 36 | uint256 _id, 37 | uint256 _price, 38 | uint256 _totalSupply, 39 | bytes32 _allowListMerkleRoot, 40 | string memory _uri 41 | ) public onlyOwner { 42 | require( 43 | _tokenTypes[_id].totalSupply == 0, 44 | "General: type already defined" 45 | ); 46 | require( 47 | _totalSupply > 0, 48 | "General: must set an above zero total supply" 49 | ); 50 | require(bytes(_uri).length > 0, "General: must set a URI"); 51 | 52 | _tokenTypes[_id] = TokenType( 53 | _price, 54 | 0, 55 | _totalSupply, 56 | _allowListMerkleRoot, 57 | _uri 58 | ); 59 | } 60 | 61 | function uri(uint256 it) 62 | public 63 | view 64 | virtual 65 | override 66 | returns (string memory) 67 | { 68 | return _tokenTypes[it].uri; 69 | } 70 | 71 | function setCanMint(bool _canMint) public onlyOwner { 72 | canMint = _canMint; 73 | } 74 | 75 | function setPrice(uint256 id, uint256 price) public onlyOwner { 76 | _tokenTypes[id].price = price; 77 | } 78 | 79 | function getUsedSupply(uint256 id) public view returns (uint256) { 80 | return _tokenTypes[id].usedSupply; 81 | } 82 | 83 | function getTotalSupply(uint256 id) public view returns (uint256) { 84 | return _tokenTypes[id].totalSupply; 85 | } 86 | 87 | function getPrice(uint256 id) public view returns (uint256) { 88 | return _tokenTypes[id].price; 89 | } 90 | 91 | function setMintApprovals(uint256 id, bytes32 merkleRoot) 92 | external 93 | onlyOwner 94 | { 95 | _tokenTypes[id].allowListMerkleRoot = merkleRoot; 96 | } 97 | 98 | function mintToMany(address[] calldata _to, uint256 _id) 99 | external 100 | onlyOwner 101 | { 102 | require( 103 | _tokenTypes[_id].usedSupply + _to.length < 104 | _tokenTypes[_id].totalSupply, 105 | "General: total supply used up" 106 | ); 107 | for (uint256 i = 0; i < _to.length; ++i) { 108 | address to = _to[i]; 109 | require( 110 | !_hasMinted[to][_id] && balanceOf(to, _id) == 0, 111 | "General: cannot own more than one of a General Card" 112 | ); 113 | _tokenTypes[_id].usedSupply++; 114 | _hasMinted[to][_id] = true; 115 | _mint(to, _id, 1, ""); 116 | } 117 | } 118 | 119 | function mint( 120 | address to, 121 | uint256 id, 122 | bytes32[] calldata merkleProof 123 | ) external payable nonReentrant { 124 | require(canMint, "General: minting is disabled"); 125 | require( 126 | _tokenTypes[id].usedSupply < _tokenTypes[id].totalSupply || 127 | _tokenTypes[id].allowListMerkleRoot == bytes32(0), 128 | "General: total supply used up" 129 | ); 130 | require( 131 | balanceOf(to, id) == 0 && !_hasMinted[to][id], 132 | "General: cannot own more than one of a General Card" 133 | ); 134 | 135 | bool allowlisted = MerkleProof.verify( 136 | merkleProof, 137 | _tokenTypes[id].allowListMerkleRoot, 138 | keccak256(abi.encodePacked(msg.sender)) 139 | ); 140 | 141 | bool isAbleToMint = _tokenTypes[id].allowListMerkleRoot == bytes32(0) || 142 | allowlisted; 143 | 144 | if (_tokenTypes[id].price > 0) { 145 | require( 146 | msg.value >= _tokenTypes[id].price || allowlisted, 147 | "General: incorrect price or not approved" 148 | ); 149 | isAbleToMint = true; 150 | } else { 151 | require( 152 | msg.value == 0, 153 | "General: sent value for non-payable token ID" 154 | ); 155 | } 156 | 157 | require(isAbleToMint, "General: not approved to mint"); 158 | 159 | _tokenTypes[id].usedSupply++; 160 | _hasMinted[to][id] = true; 161 | _mint(to, id, 1, bytes("")); 162 | } 163 | 164 | function withdraw(uint256 amount, address payable to) external onlyOwner { 165 | require(address(this).balance >= amount, "General: not enough balance"); 166 | if (amount == 0) { 167 | amount = address(this).balance; 168 | } 169 | if (to == address(0)) { 170 | to = payable(msg.sender); 171 | } 172 | to.sendValue(amount); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /contracts/cards/Invite1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "./ERC1155.sol"; 6 | import "./Whitelistable.sol"; 7 | import "@openzeppelin/contracts/access/Ownable.sol"; 8 | import "@openzeppelin/contracts/utils/Strings.sol"; 9 | import "@openzeppelin/contracts/utils/Address.sol"; 10 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 11 | 12 | contract Invite1155 is ERC1155, Ownable, Whitelistable, ReentrancyGuard { 13 | using Strings for uint256; 14 | using Address for address payable; 15 | 16 | constructor(string memory _name, string memory _symbol) ERC1155("") { 17 | name = _name; 18 | symbol = _symbol; 19 | } 20 | 21 | struct TokenType { 22 | string uri; 23 | uint256 price; 24 | uint256 usedSupply; 25 | uint256 totalSupply; 26 | } 27 | 28 | mapping(uint256 => TokenType) _tokenTypes; 29 | mapping(uint256 => mapping(address => bool)) private _mintApprovals; 30 | mapping(address => mapping(uint256 => bool)) private _hasMinted; 31 | 32 | bool private canMint; 33 | 34 | string public name; 35 | string public symbol; 36 | 37 | function createType( 38 | uint256 _id, 39 | string memory _uri, 40 | uint256 _price, 41 | uint256 _totalSupply 42 | ) public onlyOwner { 43 | require( 44 | _tokenTypes[_id].totalSupply == 0, 45 | "Invite: type already defined" 46 | ); 47 | require( 48 | _totalSupply > 0, 49 | "Invite: must set an above zero total supply" 50 | ); 51 | require(bytes(_uri).length > 0, "Invite: must set a URI"); 52 | TokenType memory tokenType; 53 | tokenType.uri = _uri; 54 | tokenType.price = _price; 55 | tokenType.usedSupply = 0; 56 | tokenType.totalSupply = _totalSupply; 57 | _tokenTypes[_id] = tokenType; 58 | } 59 | 60 | function uri(uint256 it) 61 | public 62 | view 63 | virtual 64 | override 65 | returns (string memory) 66 | { 67 | return _tokenTypes[it].uri; 68 | } 69 | 70 | function setCanMint(bool _canMint) public onlyOwner { 71 | canMint = _canMint; 72 | } 73 | 74 | function setPrice(uint256 id, uint256 price) public onlyOwner { 75 | _tokenTypes[id].price = price; 76 | } 77 | 78 | function getUsedSupply(uint256 id) public view returns (uint256) { 79 | return _tokenTypes[id].usedSupply; 80 | } 81 | 82 | function getTotalSupply(uint256 id) public view returns (uint256) { 83 | return _tokenTypes[id].totalSupply; 84 | } 85 | 86 | function getPrice(uint256 id) public view returns (uint256) { 87 | return _tokenTypes[id].price; 88 | } 89 | 90 | function mintToMany(address[] calldata _to, uint256 _id) 91 | external 92 | onlyOwner 93 | { 94 | require( 95 | _tokenTypes[_id].usedSupply + _to.length < 96 | _tokenTypes[_id].totalSupply, 97 | "Invite: total supply used up" 98 | ); 99 | for (uint256 i = 0; i < _to.length; ++i) { 100 | address to = _to[i]; 101 | require( 102 | !_hasMinted[to][_id] && balanceOf(to, _id) == 0, 103 | "Invite: cannot own more than one of an Invite" 104 | ); 105 | _tokenTypes[_id].usedSupply++; 106 | _hasMinted[to][_id] = true; 107 | _mint(to, _id, 1, ""); 108 | } 109 | } 110 | 111 | function mint(address to, uint256 id) external payable nonReentrant { 112 | require(canMint, "Invite: minting is disabled"); 113 | require( 114 | _tokenTypes[id].usedSupply + 1 < _tokenTypes[id].totalSupply, 115 | "Invite: total supply used up" 116 | ); 117 | require( 118 | balanceOf(to, id) == 0 && !_hasMinted[to][id], 119 | "Invite: cannot own more than one of an Invite" 120 | ); 121 | if (_tokenTypes[id].price > 0) { 122 | require( 123 | msg.value >= _tokenTypes[id].price || 124 | (_mintApprovals[id][to] || isWhitelisted(to, id)), 125 | "Invite: not whitelisted and msg.value is not correct price" 126 | ); 127 | } else { 128 | require( 129 | _mintApprovals[id][to] || isWhitelisted(to, id), 130 | "Invite: not approved to mint" 131 | ); 132 | require( 133 | msg.value == 0, 134 | "Invite: sent value for non-payable token ID" 135 | ); 136 | } 137 | _mintApprovals[id][to] = false; 138 | _tokenTypes[id].usedSupply++; 139 | _hasMinted[to][id] = true; 140 | _mint(to, id, 1, bytes("")); 141 | } 142 | 143 | function setMintApproval( 144 | address spender, 145 | bool value, 146 | uint256 id 147 | ) external onlyOwner { 148 | _mintApprovals[id][spender] = value; 149 | } 150 | 151 | function setMintApprovals( 152 | address[] calldata spenders, 153 | bool[] calldata values, 154 | uint256 id 155 | ) external onlyOwner { 156 | require( 157 | spenders.length == values.length, 158 | "Invite: spender and amounts arrays must be the same length" 159 | ); 160 | for (uint256 i = 0; i < spenders.length; ++i) { 161 | _mintApprovals[id][spenders[i]] = values[i]; 162 | } 163 | } 164 | 165 | function isMintApproved(address spender, uint256 id) 166 | external 167 | view 168 | returns (bool) 169 | { 170 | return _mintApprovals[id][spender]; 171 | } 172 | 173 | function canMintToken(address minter, uint256 id) 174 | external 175 | view 176 | returns (bool) 177 | { 178 | return 179 | _tokenTypes[id].price > 0 || 180 | _mintApprovals[id][minter] || 181 | isWhitelisted(minter, id); 182 | } 183 | 184 | function setWhitelistCheck( 185 | string memory specification, 186 | address tokenAddress, 187 | uint256 _id 188 | ) public virtual override onlyOwner { 189 | super.setWhitelistCheck(specification, tokenAddress, _id); 190 | } 191 | 192 | function withdraw(uint256 amount, address payable to) external onlyOwner { 193 | require(address(this).balance >= amount, "Invite: not enough balance"); 194 | if (amount == 0) { 195 | amount = address(this).balance; 196 | } 197 | if (to == address(0)) { 198 | to = payable(msg.sender); 199 | } 200 | to.sendValue(amount); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /contracts/cards/StringUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | library StringUtils { 6 | /// @dev Does a byte-by-byte lexicographical comparison of two strings. 7 | /// @return a negative number if `_a` is smaller, zero if they are equal 8 | /// and a positive numbe if `_b` is smaller. 9 | function compare(string memory _a, string memory _b) 10 | internal 11 | pure 12 | returns (int256) 13 | { 14 | bytes memory a = bytes(_a); 15 | bytes memory b = bytes(_b); 16 | uint256 minLength = a.length; 17 | if (b.length < minLength) minLength = b.length; 18 | //@todo unroll the loop into increments of 32 and do full 32 byte comparisons 19 | for (uint256 i = 0; i < minLength; i++) 20 | if (a[i] < b[i]) return -1; 21 | else if (a[i] > b[i]) return 1; 22 | if (a.length < b.length) return -1; 23 | else if (a.length > b.length) return 1; 24 | else return 0; 25 | } 26 | 27 | /// @dev Compares two strings and returns true iff they are equal. 28 | function equal(string memory _a, string memory _b) 29 | internal 30 | pure 31 | returns (bool) 32 | { 33 | return compare(_a, _b) == 0; 34 | } 35 | 36 | /// @dev Finds the index of the first occurrence of _needle in _haystack 37 | function indexOf(string memory _haystack, string memory _needle) 38 | internal 39 | pure 40 | returns (int256) 41 | { 42 | bytes memory h = bytes(_haystack); 43 | bytes memory n = bytes(_needle); 44 | if (h.length < 1 || n.length < 1 || (n.length > h.length)) return -1; 45 | else if (h.length > (2**128 - 1)) 46 | // since we have to be able to return -1 (if the char isn't found or input error), this function must return an "int" type with a max length of (2^128 - 1) 47 | return -1; 48 | else { 49 | uint256 subindex = 0; 50 | for (uint256 i = 0; i < h.length; i++) { 51 | if (h[i] == n[0]) // found the first char of b 52 | { 53 | subindex = 1; 54 | while ( 55 | subindex < n.length && 56 | (i + subindex) < h.length && 57 | h[i + subindex] == n[subindex] // search until the chars don't match or until we reach the end of a or b 58 | ) { 59 | subindex++; 60 | } 61 | if (subindex == n.length) return int256(i); 62 | } 63 | } 64 | return -1; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/cards/Whitelistable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/utils/Address.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 8 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 9 | import "./StringUtils.sol"; 10 | 11 | contract Whitelistable { 12 | using Address for address; 13 | using StringUtils for string; 14 | 15 | enum Specification { 16 | NONE, 17 | ERC721, 18 | ERC20 19 | } 20 | 21 | struct WhitelistCheck { 22 | Specification specification; 23 | address tokenAddress; 24 | } 25 | mapping(uint256 => WhitelistCheck) private whitelist; 26 | 27 | function setWhitelistCheck( 28 | string memory specification, 29 | address tokenAddress, 30 | uint256 _id 31 | ) public virtual { 32 | require(tokenAddress.isContract(), "Token address must be a contract"); 33 | whitelist[_id].specification = specificationByValue(specification); 34 | whitelist[_id].tokenAddress = tokenAddress; 35 | } 36 | 37 | function isWhitelisted(address wallet, uint256 _id) 38 | internal 39 | view 40 | returns (bool) 41 | { 42 | if (whitelist[_id].specification == Specification.ERC721) { 43 | try IERC721(whitelist[_id].tokenAddress).balanceOf(wallet) { 44 | return 45 | IERC721(whitelist[_id].tokenAddress).balanceOf(wallet) > 0; 46 | } catch Error(string memory) { 47 | return false; 48 | } 49 | } else if (whitelist[_id].specification == Specification.ERC20) { 50 | try IERC20(whitelist[_id].tokenAddress).balanceOf(wallet) { 51 | return 52 | IERC20(whitelist[_id].tokenAddress).balanceOf(wallet) > 0; 53 | } catch Error(string memory) { 54 | return false; 55 | } 56 | } else { 57 | return false; 58 | } 59 | } 60 | 61 | function specificationByValue(string memory value) 62 | private 63 | pure 64 | returns (Specification) 65 | { 66 | if (value.equal("ERC721") || value.equal("ERC-721")) { 67 | return Specification.ERC721; 68 | } else if (value.equal("ERC20") || value.equal("ERC-20")) { 69 | return Specification.ERC20; 70 | } else { 71 | revert("Unknown specification"); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /contracts/mementos/GalleryMementos.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | import "@openzeppelin/contracts/access/Ownable.sol"; 10 | import "@openzeppelin/contracts/utils/Address.sol"; 11 | import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; 12 | 13 | contract GalleryMementos is ERC1155, Ownable { 14 | using Address for address payable; 15 | 16 | struct TokenType { 17 | uint256 price; 18 | uint256 usedSupply; 19 | uint256 maxSupply; 20 | bytes32 allowListMerkleRoot; 21 | string uri; 22 | } 23 | 24 | bool public canMint; 25 | 26 | mapping(uint256 => TokenType) _tokenTypes; 27 | mapping(address => mapping(uint256 => bool)) private _hasMinted; 28 | 29 | string public constant name = "Gallery Mementos"; 30 | string public constant symbol = "GM"; 31 | 32 | constructor() ERC1155("") {} 33 | 34 | function setCanMint(bool _canMint) public onlyOwner { 35 | canMint = _canMint; 36 | } 37 | 38 | function setTokenType( 39 | uint256 _id, 40 | uint256 _price, 41 | uint256 _usedSupply, 42 | uint256 _maxSupply, 43 | bytes32 _allowListMerkleRoot, 44 | string memory _uri 45 | ) public onlyOwner { 46 | _tokenTypes[_id] = TokenType( 47 | _price, 48 | _usedSupply, 49 | _maxSupply, 50 | _allowListMerkleRoot, 51 | _uri 52 | ); 53 | } 54 | 55 | function getUsedSupply(uint256 _id) public view returns (uint256) { 56 | return _tokenTypes[_id].usedSupply; 57 | } 58 | 59 | function getMaxSupply(uint256 _id) public view returns (uint256) { 60 | return _tokenTypes[_id].maxSupply; 61 | } 62 | 63 | function getPrice(uint256 _id) public view returns (uint256) { 64 | return _tokenTypes[_id].price; 65 | } 66 | 67 | function uri(uint256 id) 68 | public 69 | view 70 | virtual 71 | override 72 | returns (string memory) 73 | { 74 | return _tokenTypes[id].uri; 75 | } 76 | 77 | function mintToMany(uint256 id, address[] calldata _to) external onlyOwner { 78 | require(canMint, "Memorabilia: minting is disabled"); 79 | require( 80 | bytes(_tokenTypes[id].uri).length > 0, 81 | "Memorabilia: no URI set" 82 | ); 83 | if (_tokenTypes[id].maxSupply > 0) { 84 | require( 85 | _to.length + _tokenTypes[id].usedSupply < 86 | _tokenTypes[id].maxSupply, 87 | "Memorabilia: max supply reached" 88 | ); 89 | } 90 | for (uint256 i = 0; i < _to.length; ++i) { 91 | address to = _to[i]; 92 | require( 93 | !_hasMinted[to][id] && balanceOf(to, id) == 0, 94 | "Memorabilia: cannot own more than one of a General Card" 95 | ); 96 | _tokenTypes[id].usedSupply++; 97 | _hasMinted[to][id] = true; 98 | _mint(to, id, 1, ""); 99 | } 100 | } 101 | 102 | function mint( 103 | uint256 id, 104 | address to, 105 | bytes32[] calldata merkleProof 106 | ) external payable { 107 | require(canMint, "Memorabilia: minting is disabled"); 108 | require( 109 | bytes(_tokenTypes[id].uri).length > 0, 110 | "Memorabilia: no URI set" 111 | ); 112 | if (_tokenTypes[id].maxSupply > 0) { 113 | require( 114 | _tokenTypes[id].usedSupply < _tokenTypes[id].maxSupply, 115 | "Memorabilia: max supply reached" 116 | ); 117 | } 118 | require( 119 | balanceOf(to, id) == 0 && !_hasMinted[to][id], 120 | "Memorabilia: cannot mint while owning poster" 121 | ); 122 | bool allowlisted = MerkleProof.verify( 123 | merkleProof, 124 | _tokenTypes[id].allowListMerkleRoot, 125 | keccak256(abi.encodePacked(msg.sender)) 126 | ); 127 | 128 | bool isAbleToMint = _tokenTypes[id].allowListMerkleRoot == bytes32(0) || 129 | allowlisted; 130 | 131 | if (_tokenTypes[id].price > 0) { 132 | require( 133 | msg.value >= _tokenTypes[id].price || allowlisted, 134 | "General: incorrect price or not approved" 135 | ); 136 | } else { 137 | require( 138 | msg.value == 0, 139 | "General: sent value for non-payable token ID" 140 | ); 141 | } 142 | 143 | require(isAbleToMint, "General: not approved to mint"); 144 | 145 | _tokenTypes[id].usedSupply++; 146 | _hasMinted[to][id] = true; 147 | _mint(to, id, 1, bytes("")); 148 | } 149 | 150 | function withdraw(uint256 amount, address payable to) external onlyOwner { 151 | require(address(this).balance >= amount, "General: not enough balance"); 152 | if (amount == 0) { 153 | amount = address(this).balance; 154 | } 155 | if (to == address(0)) { 156 | to = payable(msg.sender); 157 | } 158 | to.sendValue(amount); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /contracts/mementos/GalleryMementosMultiMinter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | import "@openzeppelin/contracts/access/Ownable.sol"; 10 | import "@openzeppelin/contracts/utils/Address.sol"; 11 | import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; 12 | 13 | contract GalleryMementosMultiMinter is ERC1155, Ownable { 14 | using Address for address payable; 15 | 16 | struct TokenType { 17 | uint256 price; 18 | uint256 usedSupply; 19 | uint256 maxSupply; 20 | uint256 maxPerWallet; 21 | bytes32 allowListMerkleRoot; 22 | string uri; 23 | } 24 | 25 | bool public canMint; 26 | 27 | mapping(uint256 => TokenType) _tokenTypes; 28 | mapping(address => mapping(uint256 => uint256)) private _walletMints; 29 | 30 | string public constant name = "Gallery Mementos"; 31 | string public constant symbol = "GM"; 32 | 33 | constructor() ERC1155("") {} 34 | 35 | function setCanMint(bool _canMint) public onlyOwner { 36 | canMint = _canMint; 37 | } 38 | 39 | function setTokenType( 40 | uint256 _id, 41 | uint256 _price, 42 | uint256 _maxSupply, 43 | uint256 _maxPerWallet, 44 | bytes32 _allowListMerkleRoot, 45 | string memory _uri 46 | ) public onlyOwner { 47 | _tokenTypes[_id] = TokenType( 48 | _price, 49 | _tokenTypes[_id].usedSupply, 50 | _maxSupply, 51 | _maxPerWallet, 52 | _allowListMerkleRoot, 53 | _uri 54 | ); 55 | } 56 | 57 | function getUsedSupply(uint256 _id) public view returns (uint256) { 58 | return _tokenTypes[_id].usedSupply; 59 | } 60 | 61 | function getMaxSupply(uint256 _id) public view returns (uint256) { 62 | return _tokenTypes[_id].maxSupply; 63 | } 64 | 65 | function getMaxMints(uint256 _id) public view returns (uint256) { 66 | return _tokenTypes[_id].maxPerWallet; 67 | } 68 | 69 | function getPrice(uint256 _id) public view returns (uint256) { 70 | return _tokenTypes[_id].price; 71 | } 72 | 73 | function uri( 74 | uint256 id 75 | ) public view virtual override returns (string memory) { 76 | return _tokenTypes[id].uri; 77 | } 78 | 79 | function mintToMany( 80 | uint256 id, 81 | address[] calldata _to, 82 | uint256[] calldata _quantities 83 | ) external onlyOwner { 84 | require(canMint, "Mementos: minting is disabled"); 85 | require(_to.length == _quantities.length, "Mementos: length mismatch"); 86 | require(bytes(_tokenTypes[id].uri).length > 0, "Mementos: no URI set"); 87 | if (_tokenTypes[id].maxSupply > 0) { 88 | require( 89 | _to.length + _tokenTypes[id].usedSupply < 90 | _tokenTypes[id].maxSupply, 91 | "Mementos: max supply reached" 92 | ); 93 | } 94 | for (uint256 i = 0; i < _to.length; ++i) { 95 | address to = _to[i]; 96 | uint256 amount = _quantities[i]; 97 | if (_tokenTypes[id].maxPerWallet > 0) { 98 | require( 99 | _walletMints[to][id] + amount <= 100 | _tokenTypes[id].maxPerWallet, 101 | "Mementos: max per wallet reached" 102 | ); 103 | } 104 | _walletMints[to][id] += amount; 105 | _tokenTypes[id].usedSupply += amount; 106 | _mint(to, id, amount, ""); 107 | } 108 | } 109 | 110 | function mint( 111 | uint256 id, 112 | address to, 113 | uint256 amount, 114 | bytes32[] calldata merkleProof 115 | ) external payable { 116 | require(canMint, "Mementos: minting is disabled"); 117 | require(bytes(_tokenTypes[id].uri).length > 0, "Mementos: no URI set"); 118 | if (_tokenTypes[id].maxSupply > 0) { 119 | require( 120 | _tokenTypes[id].usedSupply < _tokenTypes[id].maxSupply, 121 | "Mementos: max supply reached" 122 | ); 123 | } 124 | 125 | bool allowlisted = MerkleProof.verify( 126 | merkleProof, 127 | _tokenTypes[id].allowListMerkleRoot, 128 | keccak256(abi.encodePacked(msg.sender)) 129 | ); 130 | 131 | bool isAbleToMint = _tokenTypes[id].allowListMerkleRoot == bytes32(0) || 132 | allowlisted; 133 | 134 | if (_tokenTypes[id].price > 0) { 135 | require( 136 | msg.value >= _tokenTypes[id].price * amount || allowlisted, 137 | "Mementos: incorrect price or not approved" 138 | ); 139 | } else { 140 | require( 141 | msg.value == 0, 142 | "Mementos: sent value for non-payable token ID" 143 | ); 144 | } 145 | 146 | if (_tokenTypes[id].maxPerWallet > 0) { 147 | require( 148 | _walletMints[to][id] + amount <= _tokenTypes[id].maxPerWallet, 149 | "Mementos: max per wallet reached" 150 | ); 151 | } 152 | 153 | require(isAbleToMint, "Mementos: not approved to mint"); 154 | 155 | _walletMints[to][id] += amount; 156 | _tokenTypes[id].usedSupply += amount; 157 | _mint(to, id, amount, bytes("")); 158 | } 159 | 160 | function withdraw(uint256 amount, address payable to) external onlyOwner { 161 | require( 162 | address(this).balance >= amount, 163 | "Mementos: not enough balance" 164 | ); 165 | if (amount == 0) { 166 | amount = address(this).balance; 167 | } 168 | if (to == address(0)) { 169 | to = payable(msg.sender); 170 | } 171 | to.sendValue(amount); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /contracts/merch/MerchNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../ERC721A.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | import "@openzeppelin/contracts/access/Ownable.sol"; 10 | import "@openzeppelin/contracts/utils/Address.sol"; 11 | import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; 12 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 13 | import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; 14 | 15 | contract GalleryMerch is ERC721A, Ownable, EIP712 { 16 | using Address for address payable; 17 | 18 | struct MerchType { 19 | uint256 price; 20 | uint256 maxPerWallet; 21 | uint256 usedPublicSupply; 22 | uint256 publicSupply; 23 | uint256 usedReserveSupply; 24 | uint256 reserveSupply; 25 | bytes32 allowlistMerkleRoot; 26 | string uri; 27 | string redeemedURI; 28 | } 29 | 30 | bool private canMint; 31 | 32 | string public description; 33 | 34 | mapping(uint256 => bool) private _redeemed; 35 | 36 | mapping(uint256 => MerchType) private _merchTypes; 37 | mapping(uint256 => mapping(address => uint256)) private _merchOwners; 38 | mapping(uint256 => uint256) private _tokenIDToMerchType; 39 | mapping(address => uint256) private _transferNonces; 40 | 41 | constructor(string memory _description) 42 | ERC721A("Gallery Merch", "GM") 43 | EIP712("GalleryMerch", "0") 44 | { 45 | description = _description; 46 | } 47 | 48 | function tokenURI(uint256 tokenId) 49 | public 50 | view 51 | virtual 52 | override 53 | returns (string memory) 54 | { 55 | if (_redeemed[tokenId]) { 56 | return _merchTypes[_tokenIDToMerchType[tokenId]].redeemedURI; 57 | } else { 58 | return _merchTypes[_tokenIDToMerchType[tokenId]].uri; 59 | } 60 | } 61 | 62 | function setMerchType( 63 | uint256 id, 64 | uint256 price, 65 | uint256 maxPerWallet, 66 | uint256 maxPublicSupply, 67 | uint256 maxReserveSupply, 68 | bytes32 allowListMerkleRoot, 69 | string calldata uri, 70 | string calldata redeemedURI 71 | ) public onlyOwner { 72 | _merchTypes[id] = MerchType( 73 | price, 74 | maxPerWallet, 75 | _merchTypes[id].usedPublicSupply, 76 | maxPublicSupply, 77 | _merchTypes[id].usedReserveSupply, 78 | maxReserveSupply, 79 | allowListMerkleRoot, 80 | uri, 81 | redeemedURI 82 | ); 83 | } 84 | 85 | function setDescription(string calldata _description) public onlyOwner { 86 | description = _description; 87 | } 88 | 89 | function isRedeemed(uint256 tokenId) public view returns (bool) { 90 | return _redeemed[tokenId]; 91 | } 92 | 93 | function getPublicSupply(uint256 merchType) public view returns (uint256) { 94 | return _merchTypes[merchType].publicSupply; 95 | } 96 | 97 | function getReserveSupply(uint256 merchType) public view returns (uint256) { 98 | return _merchTypes[merchType].reserveSupply; 99 | } 100 | 101 | function getUsedPublicSupply(uint256 merchType) 102 | public 103 | view 104 | returns (uint256) 105 | { 106 | return _merchTypes[merchType].usedPublicSupply; 107 | } 108 | 109 | function getPrice(uint256 merchType) public view returns (uint256) { 110 | return _merchTypes[merchType].price; 111 | } 112 | 113 | function balanceOfType(uint256 merchType, address owner) 114 | public 115 | view 116 | returns (uint256) 117 | { 118 | return _merchOwners[merchType][owner]; 119 | } 120 | 121 | function isAllowlistOnly(uint256 merchType) public view returns (bool) { 122 | return _merchTypes[merchType].allowlistMerkleRoot != 0; 123 | } 124 | 125 | function getUsedReserveSupply(uint256 merchType) 126 | public 127 | view 128 | returns (uint256) 129 | { 130 | return _merchTypes[merchType].usedReserveSupply; 131 | } 132 | 133 | function setCanMint(bool _canMint) public onlyOwner { 134 | canMint = _canMint; 135 | } 136 | 137 | function setMaxPerWallet(uint256 merchType, uint256 _maxPerWallet) 138 | public 139 | onlyOwner 140 | { 141 | _merchTypes[merchType].maxPerWallet = _maxPerWallet; 142 | } 143 | 144 | function setPrice(uint256 merchType, uint256 price) public onlyOwner { 145 | _merchTypes[merchType].price = price; 146 | } 147 | 148 | function setAllowlist(uint256 merchType, bytes32 merkleRoot) 149 | external 150 | onlyOwner 151 | { 152 | _merchTypes[merchType].allowlistMerkleRoot = merkleRoot; 153 | } 154 | 155 | function setPublicSupply(uint256 merchType, uint256 supply) 156 | public 157 | onlyOwner 158 | { 159 | _merchTypes[merchType].publicSupply = supply; 160 | } 161 | 162 | function setReserveSupply(uint256 merchType, uint256 supply) 163 | public 164 | onlyOwner 165 | { 166 | _merchTypes[merchType].reserveSupply = supply; 167 | } 168 | 169 | function getTransferNonce(address owner) public view returns (uint256) { 170 | return _transferNonces[owner]; 171 | } 172 | 173 | /** 174 | * @dev See {IERC721-transferFrom}. 175 | */ 176 | function transferFrom( 177 | address from, 178 | address to, 179 | uint256 tokenId 180 | ) public virtual override { 181 | require(!_redeemed[tokenId], "Cannot transfer a redeemed token"); 182 | super.transferFrom(from, to, tokenId); 183 | } 184 | 185 | /** 186 | * @dev See {IERC721-safeTransferFrom}. 187 | */ 188 | function safeTransferFrom( 189 | address from, 190 | address to, 191 | uint256 tokenId 192 | ) public virtual override { 193 | require(!_redeemed[tokenId], "Cannot transfer a redeemed token"); 194 | super.safeTransferFrom(from, to, tokenId); 195 | } 196 | 197 | /** 198 | * @dev See {IERC721-safeTransferFrom}. 199 | */ 200 | function safeTransferFrom( 201 | address from, 202 | address to, 203 | uint256 tokenId, 204 | bytes memory _data 205 | ) public virtual override { 206 | require(!_redeemed[tokenId], "Cannot transfer a redeemed token"); 207 | super.safeTransferFrom(from, to, tokenId, _data); 208 | } 209 | 210 | function approvedTransfer( 211 | address to, 212 | uint256 tokenId, 213 | bytes memory signature, 214 | uint256 deadline 215 | ) public virtual { 216 | bytes32 digest = _hashTypedDataV4( 217 | keccak256( 218 | abi.encode( 219 | keccak256( 220 | "ApprovedTransfer(address from,uint256 tokenId,uint256 nonce,uint256 deadline)" 221 | ), 222 | msg.sender, 223 | tokenId, 224 | _transferNonces[msg.sender], 225 | deadline 226 | ) 227 | ) 228 | ); 229 | 230 | address signer = ECDSA.recover(digest, signature); 231 | require(signer == to, "ApprovedTransfer: invalid signature"); 232 | require(signer != address(0), "ECDSA: invalid signature"); 233 | 234 | require( 235 | block.timestamp < deadline, 236 | "ApprovedTransfer: signed transaction expired" 237 | ); 238 | _transferNonces[msg.sender]++; 239 | 240 | super.safeTransferFrom(msg.sender, to, tokenId); 241 | } 242 | 243 | function mint( 244 | address to, 245 | uint256 merchType, 246 | uint256 amount, 247 | bytes32[] calldata merkleProof 248 | ) external payable { 249 | require(canMint, "Merch: minting is disabled"); 250 | 251 | require( 252 | _merchTypes[merchType].allowlistMerkleRoot == bytes32(0) || 253 | MerkleProof.verify( 254 | merkleProof, 255 | _merchTypes[merchType].allowlistMerkleRoot, 256 | keccak256(abi.encodePacked(msg.sender)) 257 | ), 258 | "Merch: not allowlisted" 259 | ); 260 | 261 | require( 262 | _merchOwners[merchType][to] + amount <= 263 | _merchTypes[merchType].maxPerWallet || 264 | _merchTypes[merchType].maxPerWallet == 0, 265 | "Merch: already owns max per wallet" 266 | ); 267 | require( 268 | _merchTypes[merchType].usedPublicSupply + amount <= 269 | _merchTypes[merchType].publicSupply, 270 | "Merch: max supply reached" 271 | ); 272 | 273 | _merchOwners[merchType][to] += amount; 274 | _merchTypes[merchType].usedPublicSupply += amount; 275 | for (uint256 i = 0; i < amount; i++) { 276 | _tokenIDToMerchType[_currentIndex + i] = merchType; 277 | } 278 | 279 | require( 280 | msg.value == _merchTypes[merchType].price * amount, 281 | "Merch: incorrect price" 282 | ); 283 | 284 | _mint(to, amount, "", true); 285 | } 286 | 287 | function mintReserve( 288 | address[] calldata to, 289 | uint256[] calldata merchType, 290 | uint256[] calldata amount, 291 | bool redeemedOnMint 292 | ) external payable onlyOwner { 293 | require( 294 | to.length == merchType.length && to.length == amount.length, 295 | "Merch: invalid parameters" 296 | ); 297 | 298 | for (uint256 i = 0; i < to.length; i++) { 299 | require( 300 | _merchOwners[merchType[i]][to[i]] + amount[i] <= 301 | _merchTypes[merchType[i]].maxPerWallet || 302 | _merchTypes[merchType[i]].maxPerWallet == 0, 303 | "Merch: already owns max per wallet" 304 | ); 305 | require( 306 | _merchTypes[merchType[i]].usedReserveSupply + amount[i] <= 307 | _merchTypes[merchType[i]].reserveSupply, 308 | "Merch: max supply reached" 309 | ); 310 | _merchOwners[merchType[i]][to[i]] += amount[i]; 311 | _merchTypes[merchType[i]].usedReserveSupply += amount[i]; 312 | for (uint256 j = 0; j < amount[i]; j++) { 313 | _tokenIDToMerchType[_currentIndex + j] = merchType[i]; 314 | if (redeemedOnMint) { 315 | _redeemed[_currentIndex + j] = true; 316 | } 317 | } 318 | 319 | _mint(to[i], amount[i], "", true); 320 | } 321 | } 322 | 323 | function redeem(uint256[] calldata tokenIDs) external { 324 | for (uint256 i = 0; i < tokenIDs.length; i++) { 325 | require(ownerOf(tokenIDs[i]) == msg.sender, "Merch: not owner"); 326 | _redeemed[tokenIDs[i]] = true; 327 | } 328 | } 329 | 330 | function redeemAdmin(uint256[] calldata tokenIDs) external onlyOwner { 331 | for (uint256 i = 0; i < tokenIDs.length; i++) { 332 | _redeemed[tokenIDs[i]] = true; 333 | } 334 | } 335 | 336 | function withdraw(uint256 amount, address payable to) external onlyOwner { 337 | require(address(this).balance >= amount, "Merch: not enough balance"); 338 | if (amount == 0) { 339 | amount = address(this).balance; 340 | } 341 | if (to == address(0)) { 342 | to = payable(msg.sender); 343 | } 344 | to.sendValue(amount); 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config() 2 | require("@nomiclabs/hardhat-waffle") 3 | require("@nomiclabs/hardhat-truffle5") 4 | require("hardhat-gas-reporter") 5 | 6 | const { 7 | API_URL, 8 | PRIVATE_KEY, 9 | PUBLIC_KEY, 10 | TEST_URL, 11 | TEST_PRIVATE_KEY, 12 | TEST_PUBLIC_KEY, 13 | GAS_PRICE, 14 | } = process.env 15 | 16 | module.exports = { 17 | solidity: { 18 | version: "0.8.11", 19 | settings: { 20 | optimizer: { 21 | enabled: true, 22 | runs: 300, 23 | }, 24 | }, 25 | }, 26 | networks: { 27 | hardhat: { 28 | from: PUBLIC_KEY, 29 | }, 30 | main: { 31 | url: API_URL, 32 | accounts: [`0x${PRIVATE_KEY}`], 33 | from: PUBLIC_KEY, 34 | // must be number type; .env gets read as string 35 | gasPrice: Number(GAS_PRICE) ? Number(GAS_PRICE) : 50000000000, 36 | }, 37 | test: { 38 | url: TEST_URL, 39 | accounts: [`0x${TEST_PRIVATE_KEY}`], 40 | from: TEST_PUBLIC_KEY, 41 | }, 42 | 43 | "base-mainnet": { 44 | url: "https://mainnet.base.org", 45 | accounts: [`0x${PRIVATE_KEY}`], 46 | gasPrice: Number(GAS_PRICE) ? Number(GAS_PRICE) : 10000000000, 47 | }, 48 | 49 | "base-goerli": { 50 | url: "https://goerli.base.org", 51 | accounts: [`0x${TEST_PRIVATE_KEY}`], 52 | from: TEST_PUBLIC_KEY, 53 | gasPrice: 1000000000, 54 | }, 55 | "arb-mainnet": { 56 | url: "https://arb-mainnet.g.alchemy.com/v2/QNCdLbpuPIlw5waMnJos4eCwhrlMycDX", 57 | accounts: [`0x${PRIVATE_KEY}`], 58 | gasPrice: 200000000, 59 | }, 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /metadatas/0.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Core Team Card", 3 | "external_url": "https://gallery.so", 4 | "description": "Welcome to Gallery.\n\nThis membership card is your key to the Gallery platform and community.\n\n[gallery.so](https://gallery.so)", 5 | "image": "ipfs://QmS8CadD2FZ31dtaPN212oNeqii2awG71hLrg6RyDjPtwh", 6 | "attributes": [ 7 | { 8 | "trait_type": "Tier", 9 | "value": "Core Team" 10 | }, 11 | { 12 | "trait_type": "Card", 13 | "value": "Black" 14 | }, 15 | { "trait_type": "Background", "value": "Grey" } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /metadatas/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Contributor Card", 3 | "external_url": "https://gallery.so", 4 | "description": "Welcome to Gallery.\n\nThis membership card is your key to the Gallery platform and community.\n\n[gallery.so](https://gallery.so)", 5 | "image": "ipfs://Qmb6UaEmriBJoCiVcUDQnvxvaFsLj39SUzuyMkrZ1ugK8K", 6 | "attributes": [ 7 | { 8 | "trait_type": "Card", 9 | "value": "Blue" 10 | }, 11 | { "trait_type": "Background", "value": "Grey" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /metadatas/2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Investor Card", 3 | "external_url": "https://gallery.so", 4 | "description": "Welcome to Gallery.\n\nThis membership card is your key to the Gallery platform and community.\n\n[gallery.so](https://gallery.so)", 5 | "image": "ipfs://QmdWfm36iMWrtNfoXut5oyxCCQdFP9XcEdbz9Kg1Y8KF9h", 6 | "attributes": [ 7 | { 8 | "trait_type": "Card", 9 | "value": "Black" 10 | }, 11 | { "trait_type": "Background", "value": "Black" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /metadatas/3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Founding Member Card", 3 | "external_url": "https://gallery.so", 4 | "description": "Welcome to Gallery.\n\nThis membership card is your key to the Gallery platform and community.\n\nSpecial edition as a thank you to our 323 early users.\n\n[gallery.so](https://gallery.so)", 5 | "image": "ipfs://Qmd3PAy8LEnLCVMztBn6XL8fkYd8s1mt8ggNax7kHYnUSQ", 6 | "attributes": [ 7 | { 8 | "trait_type": "Card", 9 | "value": "Platinum" 10 | }, 11 | { "trait_type": "Background", "value": "Grey" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /metadatas/4.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VIP Card", 3 | "external_url": "https://gallery.so", 4 | "description": "Welcome to Gallery.\n\nThis membership card is your key to the Gallery platform and community.\n\n[gallery.so](https://gallery.so)", 5 | "image": "ipfs://QmVN7ySbZgBWMb6vpNHvzCSNnNd91md5BEsuGsWrbbnetF", 6 | "attributes": [ 7 | { 8 | "trait_type": "Card", 9 | "value": "Platinum" 10 | }, 11 | { "trait_type": "Background", "value": "Cream" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /metadatas/5.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gold Card", 3 | "external_url": "https://gallery.so", 4 | "description": "Welcome to Gallery.\n\nThis membership card is your key to the Gallery platform and community.\n\n[gallery.so](https://gallery.so)", 5 | "image": "ipfs://QmSnTH9G5V8HMJ23rVVcntL6j6TGHe5yiAcz9AqZEQnZJk", 6 | "attributes": [ 7 | { 8 | "trait_type": "Card", 9 | "value": "Gold" 10 | }, 11 | { "trait_type": "Background", "value": "Cream" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /metadatas/6.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Silver Card", 3 | "external_url": "https://gallery.so", 4 | "description": "Welcome to Gallery.\n\nThis membership card is your key to the Gallery platform and community.\n\n[gallery.so](https://gallery.so)", 5 | "image": "ipfs://QmawgEk3NeeKuAjnEeqLc3mtQofWYEgS1azM4UBy4vzKNu", 6 | "attributes": [ 7 | { 8 | "trait_type": "Card", 9 | "value": "Silver" 10 | }, 11 | { "trait_type": "Background", "value": "Cream" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /metadatas/7.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Membership Card", 3 | "external_url": "https://gallery.so", 4 | "description": "Welcome to Gallery.\n\nThis membership card is your key to the Gallery platform and community.\n\n[gallery.so](https://gallery.so)", 5 | "image": "ipfs://QmUxAqMXKnVQM13wDm2YjM9Cw6SzSJGK4nARWWDREm6FPk", 6 | "attributes": [ 7 | { 8 | "trait_type": "Card", 9 | "value": "Membership" 10 | }, 11 | { "trait_type": "Background", "value": "White" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /metadatas/8.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Marfa Card", 3 | "external_url": "https://gallery.so", 4 | "description": "Welcome to Gallery.\n\nThis membership card is your key to the Gallery platform and community.\n\nSpecial edition to celebrate the Art Blocks Soft Opening in Marfa in Oct 2021.\n\n[gallery.so](https://gallery.so)", 5 | "image": "ipfs://QmajmyTn5PyVaeNjR999Hf7xPSEh3sZC4JHeb16HL93x2U", 6 | "attributes": [ 7 | { 8 | "trait_type": "Card", 9 | "value": "Marfa" 10 | }, 11 | { "trait_type": "Background", "value": "Desert" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /metadatas/base-v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gallery Mementos: 1K Posts", 3 | "external_url": "https://gallery.so", 4 | "description": "After months in closed testing, we have crossed 1,000 posts on Gallery and have opened Posts to the public in Open Beta.\n\nThis release marks the evolution of Gallery into a full social app built to share art. A special thank you to our early users and testers.\n\nLearn more about it in our latest [blog post](https://gallery.mirror.xyz/HNEYLQJ6vtTYqqotGjZG7pFFn8IE71_rbZkCU0R-MSk).", 5 | "image": "ipfs://QmWyDwsJacRZAWEnqRhAAV6p3oB5xgM9Q8y76tujWNwp7V" 6 | } 7 | -------------------------------------------------------------------------------- /metadatas/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gallery Mementos: Gallery x Base", 3 | "external_url": "https://gallery.so", 4 | "description": "Introducing the Gallery x Base memento, a collectible in celebration of Gallery rolling out support for Base Chain.\n\n[gallery.so](https://gallery.so)", 5 | "image": "ipfs://QmZ948YHeNxPzngmB9d5kyhi94ypFyUej9NGcGN7RQTp84" 6 | } 7 | -------------------------------------------------------------------------------- /metadatas/beyond.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gallery Mementos: Worlds Beyond", 3 | "external_url": "https://gallery.so", 4 | "description": "Introducing the Worlds Beyond Memento, an emblem of the transformational steps Gallery is taking into the exciting world of mobile web3. This piece is both a celebration of the [public beta launch of the Gallery App](https://gallery.so/mobile) and a symbol of the endless potential of creativity on the go. The memento encapsulates the spirit of our mobile-first era, manifesting our commitment to delivering unparalleled creative experiences, right in your pocket.\n\nYou can redeem this exclusive memento by [following Gallery on Twitter](https://twitter.com/GALLERY) and replying with your ENS or Wallet Address to our announcement tweet.\n\nJoin us in commemorating this mobile milestone and mark your place in Gallery's history.", 5 | "image": "ipfs://QmeWEN4cAzSkcH48rHMFPfnkNZ6enYpFSoHV1Ur5JsQHGK" 6 | } 7 | -------------------------------------------------------------------------------- /metadatas/blooming.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gallery Mementos: Blooming Connections", 3 | "external_url": "https://gallery.so", 4 | "description": "Introducing the \"Blooming Connections\" Memento, a beautiful and symbolic representation of the growing network within the Gallery community. As a field flourishes around a pristine white cube, it symbolizes the nurturing environment Gallery offers to cultivate new connections and unlock unprecedented levels of inspiration. This limited-edition NFT commemorates the seamless integration of Twitter with Gallery, empowering collectors, curators, and creatives to connect, engage, and share their passion for art.", 5 | "image": "ipfs://QmTDY3ocDaK5UBDeaVRWJebirfLfBVcvSAPFv5gbiE3QMJ" 6 | } 7 | -------------------------------------------------------------------------------- /metadatas/card.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Card", 3 | "external_url": "https://gallery.so", 4 | "description": "Metallic membership card laser etched with the Gallery logo on the front and text design on the back.\n\nThe third entry in the (OBJECTS) Gallery merch collection.\n\nThis token may be used to claim 1 physical corresponding merch item during the redemption period.", 5 | "image": "ipfs://QmNvtzqTRsn8kQ2zR3P3WS6cxK2s8rGdZQ4Ln4DQSHYXxt", 6 | "attributes": [ 7 | { 8 | "trait_type": "Object", 9 | "value": "003" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /metadatas/general_001.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Season 001 Card", 3 | "external_url": "https://gallery.so", 4 | "description": "Welcome to Gallery.\n\nThis membership card is your key to the Gallery platform and community.\n\nSeason 001.\n\n[gallery.so](https://gallery.so)", 5 | "image": "ipfs://QmXpuzP2nVyPQEcUUkUgNtrLksgkMXLDgE9m1jPHEp6LYL", 6 | "attributes": [ 7 | { "trait_type": "Card", "value": "White" }, 8 | { "trait_type": "Season", "value": "001" }, 9 | { "trait_type": "Background", "value": "White" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /metadatas/hat.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hat", 3 | "external_url": "https://gallery.so", 4 | "description": "Black hat with embroidered text design at center front and embroidered Gallery logo at center back.\n\nThe second entry in the (OBJECTS) Gallery merch collection.\n\nThis token may be used to claim 1 physical corresponding merch item during the redemption period.", 5 | "image": "ipfs://Qmbhz8LHNuTuKrJ2xsAN9wyVnz1JANLFFywU79sGVPUWjF", 6 | "attributes": [ 7 | { 8 | "trait_type": "Object", 9 | "value": "002" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /metadatas/infinita.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gallery Mementos: Infinita Prospectus", 3 | "external_url": "https://gallery.so", 4 | "description": "Infinita Prospectus, latin for Infinite Perspectives, is a collectible from Gallery Mementos that celebrates the release of our multi-gallery feature, an evolution in the way users can express themselves on Gallery. With multi-gallery, users can create an infinite number of Galleries to express their diverse tastes and the entire spectrum of their collection.\n\nGallery Mementos are a living collection of treasures that will come to tell the story of the development of Gallery and its community. You can read more about Gallery Mementos [here](https://gallery.mirror.xyz/uoO9Fns67sYzX14eRQHiO6sXz2Ojh5qKR0-Sc0F2vZY).", 5 | "image": "ipfs://QmSQaNBPGKRuYox5ai3zekNoSG6e8d6bb1K1gpcq98uDEu" 6 | } 7 | -------------------------------------------------------------------------------- /metadatas/poster.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2022 Community Poster", 3 | "external_url": "https://gallery.so", 4 | "description": "In Spring 2022, we invited our community to sign this poster to celebrate our new [brand identity](https://gallery.mirror.xyz/1jgwdWHqYF1dUQ0YoYf-hEpd-OgJ79dZ5L00ArBQzac).\n\nThis commemorative token was made available to our users who participated in the event and were early supporters in our project.\n\nThank you for being a member of Gallery.", 5 | "image": "ipfs://QmNhbZZsYDm4P9Kz5ivCsh6JpT5CfniH1NnwfqRkGuZxRb" 6 | } 7 | -------------------------------------------------------------------------------- /metadatas/t-shirt.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "T-Shirt", 3 | "external_url": "https://gallery.so", 4 | "description": "Black short sleeve cotton t-shirt with puff-print design on left chest and both puff-print and screen-print design on full back.\n\nThe first entry in the (OBJECTS) Gallery merch collection.\n\nThis token may be used to claim 1 physical corresponding merch item during the redemption period", 5 | "image": "ipfs://QmXKsU9pnK3zM4PEHUPSCsgaSGJSH1fioE4cLWgvYu6mSh", 6 | "attributes": [ 7 | { 8 | "trait_type": "Object", 9 | "value": "001" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gallery-contracts", 3 | "devDependencies": { 4 | "@nomiclabs/hardhat-ethers": "^2.0.2", 5 | "@nomiclabs/hardhat-truffle5": "^2.0.2", 6 | "@nomiclabs/hardhat-waffle": "^2.0.1", 7 | "@nomiclabs/hardhat-web3": "^2.0.0", 8 | "@openzeppelin/contracts": "4.5.0", 9 | "chai": "^4.3.4", 10 | "dotenv": "^16.0.1", 11 | "ethereum-waffle": "^3.4.0", 12 | "ethereumjs-util": "^7.1.3", 13 | "ethers": "^5.4.5", 14 | "hardhat": "^2.6.1", 15 | "hardhat-gas-reporter": "^1.0.4", 16 | "keccak256": "^1.0.3", 17 | "merkletreejs": "^0.2.24", 18 | "node-fetch": "2.6.7", 19 | "web3": "^1.6.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/comma.js: -------------------------------------------------------------------------------- 1 | // will add a comma to the end of every line in the file 2 | 3 | var fs = require("fs") 4 | var path = require("path") 5 | 6 | var file = process.argv[2] 7 | 8 | fs.readFile(file, "utf8", function (err, data) { 9 | if (err) throw err 10 | var lines = data.split("\n") 11 | var newLines = lines.map(function (line) { 12 | return line + "," 13 | }) 14 | var newFile = newLines.join("\n") 15 | 16 | fs.writeFile(file, newFile, function (err) { 17 | if (err) throw err 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /scripts/general/createType.js: -------------------------------------------------------------------------------- 1 | const { MerkleTree } = require("../helpers/merkleTree") 2 | 3 | async function main() { 4 | const contract = await ethers.getContractAt( 5 | "GeneralCards", 6 | process.env.TESTNET_GENERAL_CONTRACT_ADDRESS, 7 | await ethers.getSigner() 8 | ) 9 | 10 | const elements = [] 11 | const tree = new MerkleTree(elements) 12 | 13 | const root = tree.getHexRoot() 14 | 15 | const result = await contract.createType( 16 | 0, 17 | 0, 18 | 10000, 19 | root, 20 | "ipfs://QmVrnp71dStgCiradDrsGDj4oV2bMcyQwCQoPvkbe4au31" 21 | ) 22 | console.log("Tx: ", result.hash) 23 | } 24 | 25 | main() 26 | .then(() => process.exit(0)) 27 | .catch(error => { 28 | console.error(error) 29 | process.exit(1) 30 | }) 31 | -------------------------------------------------------------------------------- /scripts/general/deploy.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const Contract = await ethers.getContractFactory("GeneralCards") 3 | const contract = await Contract.deploy() 4 | console.log("Contract deployed to address:", contract.address) 5 | } 6 | 7 | main() 8 | .then(() => process.exit(0)) 9 | .catch(error => { 10 | console.error(error) 11 | process.exit(1) 12 | }) 13 | -------------------------------------------------------------------------------- /scripts/general/mint.js: -------------------------------------------------------------------------------- 1 | const { BigNumber } = require("@ethersproject/bignumber") 2 | const { MerkleTree } = require("../helpers/merkleTree") 3 | 4 | async function main() { 5 | const contract = await ethers.getContractAt( 6 | "GeneralCards", 7 | process.env.TESTNET_GENERAL_CONTRACT_ADDRESS, 8 | await ethers.getSigner() 9 | ) 10 | 11 | let minter = "" 12 | 13 | const elements = [ 14 | /* addresses that are whitelisted */ 15 | ] 16 | const tree = new MerkleTree(elements) 17 | 18 | const proof = tree.getHexProof(minter) 19 | console.log(proof) 20 | const result = await contract.mint(minter, 0, proof) 21 | console.log("Tx: ", result.hash) 22 | } 23 | 24 | main() 25 | .then(() => process.exit(0)) 26 | .catch(error => { 27 | console.error(error) 28 | process.exit(1) 29 | }) 30 | -------------------------------------------------------------------------------- /scripts/general/setCanMint.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GeneralCards", 4 | process.env.GENERAL_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.setCanMint(true) 8 | console.log("Tx: ", result.hash) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/helpers/merkleTree.js: -------------------------------------------------------------------------------- 1 | // test/helpers/merkleTree.js 2 | // SPDX-License-Identifier: MIT 3 | // copied from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.1/test/helpers/merkleTree.js 4 | 5 | const { bufferToHex } = require("ethereumjs-util") 6 | const web3 = require("web3") 7 | 8 | class MerkleTree { 9 | constructor(elements) { 10 | // Filter empty strings and hash elements 11 | this.elements = elements 12 | .filter(el => el) 13 | .map(el => 14 | Buffer.from( 15 | web3.utils.hexToBytes(web3.utils.sha3(el, { encoding: "hex" })) 16 | ) 17 | ) 18 | // Sort elements 19 | this.elements.sort(Buffer.compare) 20 | 21 | // Deduplicate elements 22 | this.elements = this.bufDedup(this.elements) 23 | 24 | // Create layers 25 | this.layers = this.getLayers(this.elements) 26 | } 27 | 28 | getLayers(elements) { 29 | if (elements.length === 0) { 30 | return [[""]] 31 | } 32 | 33 | const layers = [] 34 | layers.push(elements) 35 | 36 | // Get next layer until we reach the root 37 | while (layers[layers.length - 1].length > 1) { 38 | layers.push(this.getNextLayer(layers[layers.length - 1])) 39 | } 40 | 41 | return layers 42 | } 43 | 44 | getNextLayer(elements) { 45 | return elements.reduce((layer, el, idx, arr) => { 46 | if (idx % 2 === 0) { 47 | // Hash the current element with its pair element 48 | layer.push(this.combinedHash(el, arr[idx + 1])) 49 | } 50 | 51 | return layer 52 | }, []) 53 | } 54 | 55 | combinedHash(first, second) { 56 | if (!first) { 57 | return second 58 | } 59 | if (!second) { 60 | return first 61 | } 62 | 63 | return Buffer.from( 64 | web3.utils.hexToBytes( 65 | web3.utils.sha3(this.sortAndConcat(first, second), { encoding: "hex" }) 66 | ) 67 | ) 68 | } 69 | 70 | getRoot() { 71 | return this.layers[this.layers.length - 1][0] 72 | } 73 | 74 | getHexRoot() { 75 | return bufferToHex(this.getRoot()) 76 | } 77 | 78 | getProof(el) { 79 | let idx = this.bufIndexOf(el, this.elements) 80 | 81 | if (idx === -1) { 82 | throw new Error("Element does not exist in Merkle tree") 83 | } 84 | 85 | return this.layers.reduce((proof, layer) => { 86 | const pairElement = this.getPairElement(idx, layer) 87 | 88 | if (pairElement) { 89 | proof.push(pairElement) 90 | } 91 | 92 | idx = Math.floor(idx / 2) 93 | 94 | return proof 95 | }, []) 96 | } 97 | 98 | getHexProof(el) { 99 | const proof = this.getProof(el) 100 | 101 | return this.bufArrToHexArr(proof) 102 | } 103 | 104 | getPairElement(idx, layer) { 105 | const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1 106 | 107 | if (pairIdx < layer.length) { 108 | return layer[pairIdx] 109 | } else { 110 | return null 111 | } 112 | } 113 | 114 | bufIndexOf(el, arr) { 115 | let hash 116 | 117 | // Convert element to 32 byte hash if it is not one already 118 | if (el.length !== 32 || !Buffer.isBuffer(el)) { 119 | hash = Buffer.from( 120 | web3.utils.hexToBytes(web3.utils.sha3(el, { encoding: "hex" })) 121 | ) 122 | } else { 123 | hash = el 124 | } 125 | 126 | for (let i = 0; i < arr.length; i++) { 127 | if (hash.equals(arr[i])) { 128 | return i 129 | } 130 | } 131 | 132 | return -1 133 | } 134 | 135 | bufDedup(elements) { 136 | return elements.filter((el, idx) => { 137 | return idx === 0 || !elements[idx - 1].equals(el) 138 | }) 139 | } 140 | 141 | bufArrToHexArr(arr) { 142 | if (arr.some(el => !Buffer.isBuffer(el))) { 143 | throw new Error("Array is not an array of buffers") 144 | } 145 | 146 | return arr.map(el => "0x" + el.toString("hex")) 147 | } 148 | 149 | sortAndConcat(...args) { 150 | return Buffer.concat([...args].sort(Buffer.compare)) 151 | } 152 | } 153 | 154 | module.exports = { 155 | MerkleTree, 156 | } 157 | -------------------------------------------------------------------------------- /scripts/mementos/canMint.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMementosMultiMinter", 4 | process.env.BASE_MEMENTOS_MULTI_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.canMint() 8 | console.log("Can Mint: ", result) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/mementos/deploy.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const Contract = await ethers.getContractFactory("GalleryMementosMultiMinter") 3 | const contract = await Contract.deploy() 4 | console.log("Contract deployed to address:", contract.address) 5 | } 6 | 7 | main() 8 | .then(() => process.exit(0)) 9 | .catch(error => { 10 | console.error(error) 11 | process.exit(1) 12 | }) 13 | -------------------------------------------------------------------------------- /scripts/mementos/getMaxSupply.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMementosMultiMinter", 4 | process.env.BASE_MEMENTOS_MULTI_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.getMaxSupply(0) 8 | console.log("Max: ", result.toNumber()) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/mementos/getPrice.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMementosMultiMinter", 4 | process.env.BASE_MEMENTOS_MULTI_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.getPrice(3) 8 | console.log("Price: ", result.toNumber()) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/mementos/getUsedSupply.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMementosMultiMinter", 4 | process.env.BASE_MEMENTOS_MULTI_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.getUsedSupply(0) 8 | console.log("Used: ", result.toNumber()) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/mementos/lower.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | let o = JSON.parse(fs.readFileSync("allowlist.json")) 3 | let combined = o 4 | 5 | // lowercase every string in array 6 | const lowercase = arr => { 7 | return arr.map(item => item.toLowerCase()) 8 | } 9 | 10 | // dedupe array 11 | const dedupe = arr => { 12 | return [...new Set(arr)] 13 | } 14 | 15 | const final = dedupe(lowercase(combined)) 16 | console.log(`Len: ${final.length}`) 17 | fs.writeFileSync("snapshot.json", JSON.stringify(final)) 18 | -------------------------------------------------------------------------------- /scripts/mementos/mint.js: -------------------------------------------------------------------------------- 1 | const { BigNumber } = require("@ethersproject/bignumber") 2 | const { MerkleTree } = require("../helpers/merkleTree") 3 | 4 | async function main() { 5 | const contract = await ethers.getContractAt( 6 | "GalleryMementosMultiMinter", 7 | process.env.BASE_MEMENTOS_MULTI_CONTRACT_ADDRESS, 8 | await ethers.getSigner() 9 | ) 10 | 11 | let minter = "0x9a3f9764B21adAF3C6fDf6f947e6D3340a3F8AC5" 12 | 13 | // const elements = ["0x456d569592f15af845d0dbe984c12bab8f430e31"]; 14 | // const tree = new MerkleTree(elements); 15 | 16 | // const proof = tree.getHexProof(minter); 17 | // console.log(proof); 18 | const proof = [] 19 | const result = await contract.mint(4, minter, proof) 20 | console.log("Tx: ", result.hash) 21 | } 22 | 23 | main() 24 | .then(() => process.exit(0)) 25 | .catch(error => { 26 | console.error(error) 27 | process.exit(1) 28 | }) 29 | -------------------------------------------------------------------------------- /scripts/mementos/mintMany.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | let mintTo = ["groovi.eth"] 3 | 4 | console.log(`minting to ${mintTo.length} addresses`) 5 | const contract = await ethers.getContractAt( 6 | "GalleryMementosMultiMinter", 7 | process.env.BASE_MEMENTOS_MULTI_CONTRACT_ADDRESS, 8 | await ethers.getSigner() 9 | ) 10 | const result = await contract.mintToMany(2, mintTo) 11 | // console.log(result.toNumber()) 12 | console.log("Tx: ", result.hash) 13 | } 14 | 15 | main() 16 | .then(() => process.exit(0)) 17 | .catch(error => { 18 | console.error(error) 19 | process.exit(1) 20 | }) 21 | -------------------------------------------------------------------------------- /scripts/mementos/setCanMint.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMementosMultiMinter", 4 | process.env.BASE_MEMENTOS_MULTI_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.setCanMint(true) 8 | console.log("Tx: ", result.hash) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/mementos/setTokenType.js: -------------------------------------------------------------------------------- 1 | const { ethers } = require("hardhat") 2 | const { MerkleTree } = require("../helpers/merkleTree") 3 | 4 | async function main() { 5 | const contract = await ethers.getContractAt( 6 | "GalleryMementosMultiMinter", 7 | process.env.BASE_MEMENTOS_MULTI_CONTRACT_ADDRESS, 8 | await ethers.getSigner() 9 | ) 10 | 11 | // const elements = [] 12 | 13 | // const tree = new MerkleTree(elements) 14 | 15 | // const root = tree.getHexRoot() 16 | // console.log(root) 17 | 18 | const result = await contract.setTokenType( 19 | 0, 20 | ethers.utils.parseEther("0.000777"), 21 | 1000000000, 22 | 0, 23 | "0x0000000000000000000000000000000000000000000000000000000000000000", 24 | "ipfs://QmVKfCRT2jVfX9HTbNWXJFRwne7mDctdmJsHifgM4pzLSH" 25 | ) 26 | console.log("Tx: ", result.hash) 27 | } 28 | 29 | main() 30 | .then(() => process.exit(0)) 31 | .catch(error => { 32 | console.error(error) 33 | process.exit(1) 34 | }) 35 | -------------------------------------------------------------------------------- /scripts/mementos/uri.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMementosMultiMinter", 4 | process.env.BASE_MEMENTOS_MULTI_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.uri(0) 8 | console.log(result) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/mementos/withdraw.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMementosMultiMinter", 4 | process.env.BASE_MEMENTOS_MULTI_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.withdraw( 8 | 0, 9 | "0x3b341e64E7F4EFe753a31AC5114164Fa4b3149a4" 10 | ) 11 | console.log("Tx: ", result.hash) 12 | } 13 | 14 | main() 15 | .then(() => process.exit(0)) 16 | .catch(error => { 17 | console.error(error) 18 | process.exit(1) 19 | }) 20 | -------------------------------------------------------------------------------- /scripts/merch/deploy.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const Contract = await ethers.getContractFactory("GalleryMerch"); 3 | const contract = await Contract.deploy( 4 | "Gallery presents its first merch collection: “(OBJECTS)”\n\nThis is a collection of objects inspired by Gallery’s simple and refined design language.\n\nEach object exists in digital and physical form, enabling owners to express their membership in the Gallery community anywhere.\n\nEach NFT can be used to claim its physical counterpart during the redemption period, once per token. If you are purchasing a token on the secondary market with the intent of redeeming a physical object, be sure to double check that token’s metadata and confirm that it has not been redeemed yet." 5 | ); 6 | console.log("Contract deployed to address:", contract.address); 7 | } 8 | 9 | main() 10 | .then(() => process.exit(0)) 11 | .catch((error) => { 12 | console.error(error); 13 | process.exit(1); 14 | }); 15 | -------------------------------------------------------------------------------- /scripts/merch/description.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMerch", 4 | process.env.MERCH_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ); 7 | const result = await contract.description(); 8 | console.log("Description: ", result); 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch((error) => { 14 | console.error(error); 15 | process.exit(1); 16 | }); 17 | -------------------------------------------------------------------------------- /scripts/merch/encode.js: -------------------------------------------------------------------------------- 1 | const { utils, constants } = require("ethers"); 2 | 3 | /* 4 | string memory _description 5 | 6 | 7 | "Gallery presents its first merch collection: “(OBJECTS)”\n\nThis is a collection of objects inspired by Gallery’s simple and refined design language.\n\nEach object exists in digital and physical form, enabling owners to express their membership in the Gallery community anywhere.\n\nEach NFT can be used to claim its physical counterpart during the redemption period, once per token. If you are purchasing a token on the secondary market with the intent of redeeming a physical object, be sure to double check that token’s metadata and confirm that it has not been redeemed yet." 8 | */ 9 | 10 | // let res = utils.defaultAbiCoder.encode( 11 | // ["string"], 12 | // [ 13 | // "Gallery presents its first merch collection: “(OBJECTS)”\n\nThis is a collection of objects inspired by Gallery’s simple and refined design language.\n\nEach object exists in digital and physical form, enabling owners to express their membership in the Gallery community anywhere.\n\nEach NFT can be used to claim its physical counterpart during the redemption period, once per token. If you are purchasing a token on the secondary market with the intent of redeeming a physical object, be sure to double check that token’s metadata and confirm that it has not been redeemed yet.", 14 | // ] 15 | // ); 16 | 17 | console.log(constants.HashZero); 18 | -------------------------------------------------------------------------------- /scripts/merch/getReserveSupply.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMerch", 4 | process.env.MERCH_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.getReserveSupply(2) 8 | console.log("Redeemed: ", result.toNumber()) 9 | 10 | const nextResult = await contract.getUsedReserveSupply(2) 11 | console.log("Used: ", nextResult.toNumber()) 12 | } 13 | 14 | main() 15 | .then(() => process.exit(0)) 16 | .catch(error => { 17 | console.error(error) 18 | process.exit(1) 19 | }) 20 | -------------------------------------------------------------------------------- /scripts/merch/isRedeemed.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMerch", 4 | process.env.MERCH_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ); 7 | const result = await contract.isRedeemed(87); 8 | console.log("Redeemed: ", result); 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch((error) => { 14 | console.error(error); 15 | process.exit(1); 16 | }); 17 | -------------------------------------------------------------------------------- /scripts/merch/mint.js: -------------------------------------------------------------------------------- 1 | const { MerkleTree } = require("../helpers/merkleTree"); 2 | 3 | async function main() { 4 | const contract = await ethers.getContractAt( 5 | "GalleryMerch", 6 | process.env.MERCH_CONTRACT_ADDRESS, 7 | await ethers.getSigner() 8 | ); 9 | 10 | const elements = ["0x456d569592f15af845d0dbe984c12bab8f430e32"]; 11 | const tree = new MerkleTree(elements); 12 | 13 | const proof = tree.getHexProof("0x456d569592f15af845d0dbe984c12bab8f430e32"); 14 | 15 | const result = await contract.mint( 16 | "0x456d569592f15af845d0dbe984c12bab8f430e31", 17 | 0, 18 | 1, 19 | proof, 20 | { value: ethers.utils.parseEther("0.1") } 21 | ); 22 | console.log("Tx: ", result.hash); 23 | } 24 | 25 | main() 26 | .then(() => process.exit(0)) 27 | .catch((error) => { 28 | console.error(error); 29 | process.exit(1); 30 | }); 31 | -------------------------------------------------------------------------------- /scripts/merch/mintReserve.js: -------------------------------------------------------------------------------- 1 | const shirts = ["fraserstanley.eth", "mikevp.eth"] 2 | const hats = ["fraserstanley.eth", "l444u.eth"] 3 | const cards = ["fraserstanley.eth", "l444u.eth"] 4 | const redeemed = true 5 | 6 | async function main() { 7 | const contract = await ethers.getContractAt( 8 | "GalleryMerch", 9 | process.env.MERCH_CONTRACT_ADDRESS, 10 | await ethers.getSigner() 11 | ) 12 | 13 | const to = shirts.concat(hats).concat(cards) 14 | const merchTypes = shirts 15 | .map(() => 0) 16 | .concat(hats.map(() => 1)) 17 | .concat(cards.map(() => 2)) 18 | const amounts = Array(to.length).fill(1) 19 | 20 | const result = await contract.mintReserve(to, merchTypes, amounts, redeemed) 21 | console.log("Tx: ", result.hash) 22 | // console.log(result.toNumber()) 23 | } 24 | 25 | main() 26 | .then(() => process.exit(0)) 27 | .catch(error => { 28 | console.error(error) 29 | process.exit(1) 30 | }) 31 | -------------------------------------------------------------------------------- /scripts/merch/setCanMint.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMerch", 4 | process.env.MERCH_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ); 7 | const result = await contract.setCanMint(true); 8 | console.log("Tx: ", result.hash); 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch((error) => { 14 | console.error(error); 15 | process.exit(1); 16 | }); 17 | -------------------------------------------------------------------------------- /scripts/merch/setMerchType.js: -------------------------------------------------------------------------------- 1 | const { MerkleTree } = require("../helpers/merkleTree"); 2 | 3 | /* 4 | uint256 id, 5 | uint256 price, 6 | uint256 maxPerWallet, 7 | uint256 maxPublicSupply, 8 | uint256 maxReserveSupply, 9 | bytes32 allowListMerkleRoot, 10 | string calldata uri, 11 | string calldata redeemedURI 12 | */ 13 | async function main() { 14 | const contract = await ethers.getContractAt( 15 | "GalleryMerch", 16 | process.env.MERCH_CONTRACT_ADDRESS, 17 | await ethers.getSigner() 18 | ); 19 | 20 | const elements = []; 21 | const tree = new MerkleTree(elements); 22 | 23 | const root = tree.getHexRoot(); 24 | 25 | console.log(root); 26 | 27 | const result = await contract.setMerchType( 28 | 2, 29 | ethers.utils.parseEther("0.08"), 30 | 3, 31 | 450, 32 | 50, 33 | root, 34 | // ethers.constants.HashZero, 35 | "ipfs://QmSPdA9Gg8xAdVxWvUyGkdFKQ8YMVYnGjYcr3cGMcBH1ae", 36 | "ipfs://QmSPdA9Gg8xAdVxWvUyGkdFKQ8YMVYnGjYcr3cGMcBH1ae" 37 | ); 38 | console.log("Tx: ", result.hash); 39 | } 40 | 41 | main() 42 | .then(() => process.exit(0)) 43 | .catch((error) => { 44 | console.error(error); 45 | process.exit(1); 46 | }); 47 | -------------------------------------------------------------------------------- /scripts/merch/setReserveSupply.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMerch", 4 | process.env.MERCH_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.setReserveSupply(2, 75) 8 | console.log("Tx: ", result.hash) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/merch/withdraw.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "GalleryMerch", 4 | process.env.MERCH_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ); 7 | const result = await contract.withdraw( 8 | 0, 9 | "0x456d569592f15Af845D0dbe984C12BAB8F430e31" 10 | ); 11 | console.log("Tx: ", result.hash); 12 | } 13 | 14 | main() 15 | .then(() => process.exit(0)) 16 | .catch((error) => { 17 | console.error(error); 18 | process.exit(1); 19 | }); 20 | -------------------------------------------------------------------------------- /scripts/premium/balance.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "Invite1155", 4 | process.env.TESTNET_PREMIUM_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.balanceOf( 8 | "0xad74d773f534da4c45c8cc421acce98ff3769803", 9 | 3 10 | ) 11 | console.log(result) 12 | } 13 | 14 | main() 15 | .then(() => process.exit(0)) 16 | .catch(error => { 17 | console.error(error) 18 | process.exit(1) 19 | }) 20 | -------------------------------------------------------------------------------- /scripts/premium/balanceOfBatch.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const addresses = [] 3 | const ids = [] 4 | for (let i = 0; i < addresses.length; i++) { 5 | ids.push(8) 6 | } 7 | const contract = await ethers.getContractAt( 8 | "Invite1155", 9 | process.env.PREMIUM_CONTRACT_ADDRESS, 10 | await ethers.getSigner() 11 | ) 12 | const result = await contract.balanceOfBatch(addresses, ids) 13 | let totalHave = 0 14 | for (let i = 0; i < result.length; i++) { 15 | if (result[i].toString() !== "0") { 16 | totalHave++ 17 | } 18 | } 19 | console.log(totalHave) 20 | } 21 | 22 | main() 23 | .then(() => process.exit(0)) 24 | .catch(error => { 25 | console.error(error) 26 | process.exit(1) 27 | }) 28 | -------------------------------------------------------------------------------- /scripts/premium/createType.js: -------------------------------------------------------------------------------- 1 | const { BigNumber } = require("@ethersproject/bignumber") 2 | 3 | async function main() { 4 | const contract = await ethers.getContractAt( 5 | "Invite1155", 6 | process.env.PREMIUM_CONTRACT_ADDRESS, 7 | await ethers.getSigner() 8 | ) 9 | const result = await contract.createType(0, "", BigNumber.from(""), 500) 10 | console.log("Tx: ", result.hash) 11 | } 12 | 13 | main() 14 | .then(() => process.exit(0)) 15 | .catch(error => { 16 | console.error(error) 17 | process.exit(1) 18 | }) 19 | -------------------------------------------------------------------------------- /scripts/premium/deploy.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const Contract = await ethers.getContractFactory("Invite1155") 3 | const contract = await Contract.deploy("Gallery Membership Cards", "GMC") 4 | console.log("Contract deployed to address:", contract.address) 5 | } 6 | 7 | main() 8 | .then(() => process.exit(0)) 9 | .catch(error => { 10 | console.error(error) 11 | process.exit(1) 12 | }) 13 | -------------------------------------------------------------------------------- /scripts/premium/deployTest.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const Contract = await ethers.getContractFactory("TestNFT") 3 | const contract = await Contract.deploy("TestNFT", "TNFT") 4 | console.log("Contract deployed to address:", contract.address) 5 | } 6 | 7 | main() 8 | .then(() => process.exit(0)) 9 | .catch(error => { 10 | console.error(error) 11 | process.exit(1) 12 | }) 13 | -------------------------------------------------------------------------------- /scripts/premium/isMintApproved.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "Invite1155", 4 | process.env.PREMIUM_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.isMintApproved("", 5) 8 | console.log(result) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/premium/mintMany.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | let mintTo = [] 3 | 4 | console.log(`minting to ${mintTo.length} addresses`) 5 | const contract = await ethers.getContractAt( 6 | "Invite1155", 7 | process.env.PREMIUM_CONTRACT_ADDRESS, 8 | await ethers.getSigner() 9 | ) 10 | const result = await contract.mintToMany(mintTo, 5) 11 | // console.log(result.toNumber()) 12 | console.log("Tx: ", result.hash) 13 | } 14 | 15 | main() 16 | .then(() => process.exit(0)) 17 | .catch(error => { 18 | console.error(error) 19 | process.exit(1) 20 | }) 21 | 22 | function onlyUnique(value, index, self) { 23 | return self.indexOf(value) === index 24 | } 25 | -------------------------------------------------------------------------------- /scripts/premium/mintTest.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const mintTo = [] 3 | const contract = await ethers.getContractAt( 4 | "TestNFT", 5 | process.env.TEST_PREMIUM_CONTRACT_ADDRESS, 6 | await ethers.getSigner() 7 | ) 8 | let i = 0 9 | for (const to of mintTo) { 10 | const tx = await contract.mint(to, i) 11 | console.log("Tx: ", tx.hash) 12 | i++ 13 | } 14 | } 15 | 16 | main() 17 | .then(() => process.exit(0)) 18 | .catch(error => { 19 | console.error(error) 20 | process.exit(1) 21 | }) 22 | -------------------------------------------------------------------------------- /scripts/premium/setCanMint.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "Invite1155", 4 | process.env.PREMIUM_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.setCanMint(true) 8 | console.log("Tx: ", result.hash) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/premium/setMintApproval.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "Invite1155", 4 | process.env.PREMIUM_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.setMintApproval("", true, 5) 8 | console.log("Tx: ", result.hash) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/premium/setMintApprovals.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const whitelist = [] 3 | const whitelistValues = whitelist.map(() => true) 4 | const contract = await ethers.getContractAt( 5 | "Invite1155", 6 | process.env.PREMIUM_CONTRACT_ADDRESS, 7 | await ethers.getSigner() 8 | ) 9 | const result = await contract.setMintApprovals(whitelist, whitelistValues, 5) 10 | console.log("Tx: ", result.hash) 11 | } 12 | 13 | main() 14 | .then(() => process.exit(0)) 15 | .catch(error => { 16 | console.error(error) 17 | process.exit(1) 18 | }) 19 | -------------------------------------------------------------------------------- /scripts/premium/setPrice.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "Invite1155", 4 | process.env.PREMIUM_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | 8 | const result = await contract.setPrice(0, ethers.BigNumber.from("")) 9 | console.log("Tx: ", result.hash) 10 | } 11 | 12 | main() 13 | .then(() => process.exit(0)) 14 | .catch(error => { 15 | console.error(error) 16 | process.exit(1) 17 | }) 18 | -------------------------------------------------------------------------------- /scripts/premium/setWhitelistCheck.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "Invite1155", 4 | process.env.TESTNET_PREMIUM_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.setWhitelistCheck( 8 | "ERC721", 9 | process.env.TEST_PREMIUM_CONTRACT_ADDRESS, 10 | 7 11 | ) 12 | console.log("Tx: ", result.hash) 13 | } 14 | 15 | main() 16 | .then(() => process.exit(0)) 17 | .catch(error => { 18 | console.error(error) 19 | process.exit(1) 20 | }) 21 | -------------------------------------------------------------------------------- /scripts/premium/supply.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "Invite1155", 4 | process.env.PREMIUM_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.getTotalSupply(4) 8 | console.log(result.toNumber()) 9 | const used = await contract.getUsedSupply(4) 10 | console.log(used.toNumber()) 11 | } 12 | 13 | main() 14 | .then(() => process.exit(0)) 15 | .catch(error => { 16 | console.error(error) 17 | process.exit(1) 18 | }) 19 | -------------------------------------------------------------------------------- /scripts/premium/uri.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "Invite1155", 4 | process.env.PREMIUM_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.uri(5) 8 | console.log(result) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/premium/withdraw.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "Invite1155", 4 | process.env.PREMIUM_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.withdraw(0, "") 8 | console.log("Tx: ", result.hash) 9 | } 10 | 11 | main() 12 | .then(() => process.exit(0)) 13 | .catch(error => { 14 | console.error(error) 15 | process.exit(1) 16 | }) 17 | -------------------------------------------------------------------------------- /scripts/series/deploy.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const Contract = await ethers.getContractFactory("Series") 3 | const contract = await Contract.deploy("GallerySeries", "GS") 4 | console.log("Contract deployed to address:", contract.address) 5 | } 6 | 7 | main() 8 | .then(() => process.exit(0)) 9 | .catch(error => { 10 | console.error(error) 11 | process.exit(1) 12 | }) 13 | -------------------------------------------------------------------------------- /scripts/series/mint.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "Series", 4 | process.env.SERIES_CONTRACT_ADDRESS, 5 | await ethers.getSigner() 6 | ) 7 | 8 | let minter = "0x9a3f9764B21adAF3C6fDf6f947e6D3340a3F8AC5" 9 | 10 | const result = await contract.mint( 11 | minter, 12 | 4, 13 | "ipfs://QmebHLGavJvupB4vsxkDmqeGLTdHZUbqWXXWykjjpJCmVm" 14 | ) 15 | console.log("Tx: ", result.hash) 16 | } 17 | 18 | main() 19 | .then(() => process.exit(0)) 20 | .catch(error => { 21 | console.error(error) 22 | process.exit(1) 23 | }) 24 | -------------------------------------------------------------------------------- /scripts/uri.js: -------------------------------------------------------------------------------- 1 | async function main() { 2 | const contract = await ethers.getContractAt( 3 | "IERC1155MetadataURI", 4 | "0x495f947276749Ce646f68AC8c248420045cb7b5e", 5 | await ethers.getSigner() 6 | ) 7 | const result = await contract.uri( 8 | "0x7E59DDE2EE81595574DDD55C98300B81467A3618000000000000480000000001" 9 | ) 10 | console.log(result) 11 | } 12 | 13 | main() 14 | .then(() => process.exit(0)) 15 | .catch(error => { 16 | console.error(error) 17 | process.exit(1) 18 | }) 19 | -------------------------------------------------------------------------------- /snapshot/allowlist.js: -------------------------------------------------------------------------------- 1 | const { MerkleTree } = require("../scripts/helpers/merkleTree"); 2 | const fs = require("fs"); 3 | 4 | async function main() { 5 | const contract = await ethers.getContractAt( 6 | "GeneralCards", 7 | process.env.GENERAL_CONTRACT_ADDRESS, 8 | await ethers.getSigner() 9 | ); 10 | 11 | const elements = JSON.parse(fs.readFileSync("./snapshot/snapshot.json")); 12 | 13 | console.log(`Len: ${elements.length}`); 14 | 15 | const tree = new MerkleTree(elements); 16 | 17 | const root = tree.getHexRoot(); 18 | const result = await contract.setMintApprovals(0, root); 19 | await result.wait(); 20 | console.log("Tx: ", result.hash); 21 | } 22 | 23 | main() 24 | .then(() => process.exit(0)) 25 | .catch((error) => { 26 | console.error(error); 27 | process.exit(1); 28 | }); 29 | -------------------------------------------------------------------------------- /snapshot/snapshot.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | const fs = require("fs"); 3 | require("dotenv").config(); 4 | 5 | const openseaAPIKey = process.env.OPENSEA_API_KEY; 6 | const openseaAPIURL = "https://api.opensea.io/api/v1/assets"; 7 | const alchemyAPIURL = process.env.API_URL; 8 | const zeroAddress = "0x0000000000000000000000000000000000000000"; 9 | const toSnapshot = JSON.parse(fs.readFileSync("./snapshot/to-snapshot.json")); 10 | const { 11 | erc721Addresses, 12 | erc721TokenIDRanges, 13 | openseaCollectionNames, 14 | erc1155AddressTokenIDs, 15 | manualInclude, 16 | } = toSnapshot; 17 | 18 | const oldSnapshot = JSON.parse(fs.readFileSync("./snapshot/snapshot-old.json")); 19 | 20 | const main = async () => { 21 | let toSnapshot = []; 22 | 23 | console.log("Fetching opensea collections..."); 24 | 25 | for (let collection of openseaCollectionNames) { 26 | console.log(`Fetching assets for ${collection}`); 27 | let snapshot = await fetchCollectionAssets(collection, 0, 0); 28 | toSnapshot = toSnapshot.concat(snapshot); 29 | } 30 | 31 | console.log("Fetching erc1155 assets..."); 32 | 33 | for (const [address, tokenID] of erc1155AddressTokenIDs) { 34 | console.log( 35 | `Fetching ERC-1155 assets for ${address} and tokenID ${tokenID}` 36 | ); 37 | const owners = await fetchAlchemyOwners(address, tokenID); 38 | toSnapshot = toSnapshot.concat(owners); 39 | } 40 | 41 | console.log("Fetching ERC-721 token ID ranges..."); 42 | 43 | for (const [address, range] of erc721TokenIDRanges) { 44 | console.log(`Fetching ranged assets for ${address} and range ${range}`); 45 | const assets = await fetchAssetsOfRange(address, range, 0); 46 | let owners = validate(assets); 47 | owners = lowercase(owners); 48 | owners = dedupe(owners); 49 | toSnapshot = toSnapshot.concat(owners); 50 | console.log(`Fetched ${assets.length} assets, ${owners.length} owners`); 51 | } 52 | 53 | console.log("Fetching ERC-721 assets..."); 54 | 55 | for (let address of erc721Addresses) { 56 | console.log(`Fetching assets for ${address}`); 57 | let snapshot = await fetchAssets(address, 0, 0); 58 | toSnapshot = toSnapshot.concat(snapshot); 59 | } 60 | 61 | // Adding old snapshot and manual addresses 62 | 63 | toSnapshot = toSnapshot.concat(manualInclude); 64 | toSnapshot = toSnapshot.concat(oldSnapshot); 65 | 66 | // Validation 67 | 68 | toSnapshot = validate(toSnapshot); 69 | toSnapshot = lowercase(toSnapshot); 70 | toSnapshot = dedupe(toSnapshot); 71 | 72 | console.log(`Fetched ${toSnapshot.length} assets`); 73 | 74 | fs.writeFileSync("./snapshot/snapshot.json", JSON.stringify(toSnapshot)); 75 | console.log("Done!"); 76 | }; 77 | 78 | const fetchAssets = async (contractAddress, cursor, retry) => { 79 | let toSnapshot = []; 80 | 81 | let url = `${openseaAPIURL}?limit=50&order_direction=desc&asset_contract_address=${contractAddress}`; 82 | if (cursor) { 83 | url += `&cursor=${cursor}`; 84 | } 85 | const res = await fetch(url, { 86 | headers: { 87 | "X-API-KEY": openseaAPIKey, 88 | }, 89 | }); 90 | console.log("Fetched opensea!"); 91 | if (!res.ok) { 92 | if (res.status === 429 && retry < 3) { 93 | console.log("Too many requests, sleeping for a bit..."); 94 | await sleep(10000 + retry * 5000); 95 | return fetchAssets(contractAddress, cursor, retry + 1); 96 | } 97 | const text = await res.text(); 98 | 99 | throw new Error(`${res.status} ${res.statusText} ${text}`); 100 | } 101 | console.log("Parsing opensea..."); 102 | const json = await res.json(); 103 | const assets = json.assets; 104 | for (const asset of assets) { 105 | let { address } = asset.owner; 106 | if (address === zeroAddress) { 107 | continue; 108 | } 109 | 110 | toSnapshot.push(address); 111 | } 112 | 113 | if (json.next) { 114 | let next = await fetchAssets(contractAddress, json.next, 0); 115 | toSnapshot = toSnapshot.concat(next); 116 | } 117 | 118 | return toSnapshot; 119 | }; 120 | 121 | const fetchAssetsOfRange = async (contractAddress, range, retry) => { 122 | let toSnapshot = []; 123 | console.log("fetchAssetsOfRange", { contractAddress, range, retry }); 124 | if (range[0] > range[1]) { 125 | return toSnapshot; 126 | } 127 | 128 | let url = `${openseaAPIURL}?limit=30&order_direction=desc&asset_contract_address=${contractAddress}`; 129 | for (let i = range[0]; i <= range[1] && i < 30 + range[0]; i++) { 130 | url += `&token_ids=${i}`; 131 | } 132 | const res = await fetch(url, { 133 | headers: { 134 | "X-API-KEY": openseaAPIKey, 135 | }, 136 | }); 137 | console.log("Fetched opensea!"); 138 | if (!res.ok) { 139 | if (res.status === 429 && retry < 3) { 140 | console.log("Too many requests, sleeping for a bit..."); 141 | await sleep(10000 + retry * 5000); 142 | return fetchAssetsOfRange(contractAddress, range, retry + 1); 143 | } 144 | const text = await res.text(); 145 | 146 | throw new Error(`${res.status} ${res.statusText} ${text}`); 147 | } 148 | console.log("Parsing opensea..."); 149 | const json = await res.json(); 150 | const assets = json.assets; 151 | for (const asset of assets) { 152 | let { address } = asset.owner; 153 | if (address === zeroAddress) { 154 | continue; 155 | } 156 | 157 | toSnapshot.push(address); 158 | } 159 | 160 | if (assets.length === 30) { 161 | const newRange = [range[0] + 30, range[1]]; 162 | const next = await fetchAssetsOfRange(contractAddress, newRange, 0); 163 | toSnapshot = toSnapshot.concat(next); 164 | } 165 | 166 | return toSnapshot; 167 | }; 168 | 169 | const fetchCollectionAssets = async (collection, cursor, retry) => { 170 | let toSnapshot = []; 171 | 172 | let url = `${openseaAPIURL}?limit=50&order_direction=desc&collection=${collection}`; 173 | if (cursor) { 174 | url += `&cursor=${cursor}`; 175 | } 176 | const res = await fetch(url, { 177 | headers: { 178 | "X-API-KEY": openseaAPIKey, 179 | }, 180 | }); 181 | console.log("Fetched opensea!"); 182 | if (!res.ok) { 183 | if (res.status === 429 && retry < 3) { 184 | console.log("Too many requests, sleeping for a bit..."); 185 | await sleep(5000 + retry * 1000); 186 | return fetchCollectionAssets(collection, cursor, retry + 1); 187 | } 188 | const text = await res.text(); 189 | 190 | throw new Error(`${res.status} ${res.statusText} ${text}`); 191 | } 192 | console.log("Parsing opensea..."); 193 | const json = await res.json(); 194 | const assets = json.assets; 195 | for (const asset of assets) { 196 | let { address } = asset.owner; 197 | if (address === zeroAddress) { 198 | continue; 199 | } 200 | 201 | toSnapshot.push(address); 202 | } 203 | 204 | if (json.next) { 205 | let next = await fetchCollectionAssets(collection, json.next, 0); 206 | toSnapshot = toSnapshot.concat(next); 207 | } 208 | 209 | return toSnapshot; 210 | }; 211 | 212 | const fetchAlchemyOwners = async (address, tokenID) => { 213 | const url = `${alchemyAPIURL}/getOwnersForToken?contractAddress=${address}&tokenId=${tokenID}`; 214 | const resp = await fetch(url); 215 | if (!resp.ok) { 216 | const text = await resp.text(); 217 | throw new Error(`${resp.status} ${resp.statusText} ${text}`); 218 | } 219 | console.log("Fetched alchemy!"); 220 | const json = await resp.json(); 221 | return json.owners; 222 | }; 223 | 224 | function sleep(ms) { 225 | return new Promise((resolve) => { 226 | setTimeout(resolve, ms); 227 | }); 228 | } 229 | 230 | const validate = (arr) => { 231 | return arr.filter((item) => { 232 | if (item.length !== 42) { 233 | throw new Error(`ERROR: address length is messed up for ${item}!`); 234 | } 235 | return item.length === 42; 236 | }); 237 | }; 238 | // lowercase every string in array 239 | const lowercase = (arr) => { 240 | return arr.map((item) => item.toLowerCase()); 241 | }; 242 | 243 | // dedupe array 244 | const dedupe = (arr) => { 245 | return [...new Set(arr)]; 246 | }; 247 | 248 | main() 249 | .catch((error) => { 250 | console.error(error); 251 | process.exit(1); 252 | }) 253 | .then(() => process.exit(0)); 254 | -------------------------------------------------------------------------------- /snapshot/to-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "erc721Addresses": [], 3 | "erc721TokenIDRanges": [], 4 | "openseaCollectionNames": [], 5 | "erc1155AddressTokenIDs": [], 6 | "manualInclude": [] 7 | } 8 | -------------------------------------------------------------------------------- /snapshot/validateSnapshot.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const snap = fs.readFileSync("./snapshot/snapshot.json", "utf8"); 4 | const snapArray = JSON.parse(snap); 5 | const deduped = snapArray.filter( 6 | (item, index) => snapArray.indexOf(item) === index 7 | ); 8 | 9 | const lowercased = deduped.map((item) => item.toLowerCase()); 10 | 11 | fs.writeFileSync( 12 | "./snapshot/snapshot-validated.json", 13 | JSON.stringify(lowercased) 14 | ); 15 | -------------------------------------------------------------------------------- /test/General.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai") 2 | const { ethers } = require("hardhat") 3 | const { MerkleTree } = require("../scripts/helpers/merkleTree.js") 4 | 5 | describe("General", function () { 6 | let General 7 | let general 8 | let tree 9 | let proof 10 | let incorrectProof 11 | beforeEach(async function () { 12 | const [signer1, signer2] = await ethers.getSigners() 13 | General = await ethers.getContractFactory("GeneralCards") 14 | general = await General.deploy() 15 | 16 | const elements = [signer1.address, signer2.address] 17 | tree = new MerkleTree(elements) 18 | 19 | proof = tree.getHexProof(signer1.address) 20 | incorrectProof = tree.getHexProof(signer2.address) 21 | 22 | general.createType(0, 0, 100, tree.getHexRoot(), "URI 0") 23 | general.createType(1, 0, 1, tree.getHexRoot(), "URI 1") 24 | general.createType( 25 | 2, 26 | ethers.BigNumber.from("100000000000000000"), 27 | 100, 28 | tree.getHexRoot(), 29 | "URI 2" 30 | ) 31 | 32 | general.setCanMint(true) 33 | }) 34 | 35 | it("Should mint a token", async function () { 36 | const [john] = await ethers.getSigners() 37 | const resultMint = await general.connect(john).mint(john.address, 0, proof) 38 | await resultMint.wait() 39 | expect(await general.balanceOf(john.address, 0)).to.equal(1) 40 | }) 41 | 42 | it("Fails to mint because wrong merkle proof", async function () { 43 | const [john] = await ethers.getSigners() 44 | 45 | await expect( 46 | general.connect(john).mint(john.address, 0, incorrectProof) 47 | ).to.be.revertedWith("General: not approved to mint") 48 | }) 49 | 50 | it("Fails to mint 2 general cards of the same type to the same address", async function () { 51 | const signers = await ethers.getSigners() 52 | 53 | const resultMint = await general 54 | .connect(signers[0]) 55 | .mint(signers[0].address, 0, proof) 56 | await resultMint.wait() 57 | 58 | let addrs = [signers[0].address, signers[0].address] 59 | await expect(general.mintToMany(addrs, 0)).to.be.revertedWith( 60 | "General: cannot own more than one of a General Card" 61 | ) 62 | }) 63 | it("Fails to mint 2 general cards because total supply is 1", async function () { 64 | const signers = await ethers.getSigners() 65 | 66 | let addrs = [signers[0].address, signers[1].address] 67 | await expect(general.mintToMany(addrs, 1)).to.be.revertedWith( 68 | "General: total supply used up" 69 | ) 70 | }) 71 | it("Fails to mint because was sent general card", async function () { 72 | const signers = await ethers.getSigners() 73 | 74 | let addrs = [signers[0].address, signers[1].address] 75 | await general.mintToMany(addrs, 0) 76 | await general 77 | .connect(signers[0]) 78 | .safeTransferFrom( 79 | signers[0].address, 80 | signers[2].address, 81 | 0, 82 | 1, 83 | ethers.utils.toUtf8Bytes("") 84 | ) 85 | await expect( 86 | general.mintToMany([signers[2].address], 0) 87 | ).to.be.revertedWith("General: cannot own more than one of a General Card") 88 | }) 89 | it("Fails to withdraw because no balance", async function () { 90 | const [signer] = await ethers.getSigners() 91 | await expect(general.withdraw(100, signer.address)).to.be.revertedWith( 92 | "General: not enough balance" 93 | ) 94 | }) 95 | it("Fails to setCanMint because not owner", async function () { 96 | const [_, next] = await ethers.getSigners() 97 | await expect(general.connect(next).setCanMint(true)).to.be.revertedWith( 98 | "Ownable: caller is not the owner" 99 | ) 100 | }) 101 | it("Fails to setMintApproval because not owner", async function () { 102 | const [_, next] = await ethers.getSigners() 103 | await expect( 104 | general.connect(next).setMintApprovals(0, tree.getHexRoot()) 105 | ).to.be.revertedWith("Ownable: caller is not the owner") 106 | }) 107 | it("Fails to mintToMany because not owner", async function () { 108 | const [signer, next] = await ethers.getSigners() 109 | await expect( 110 | general.connect(next).mintToMany([signer.address], 0) 111 | ).to.be.revertedWith("Ownable: caller is not the owner") 112 | }) 113 | 114 | it("Approved address fails to mint token by sending price to non-priced token", async function () { 115 | const [signer] = await ethers.getSigners() 116 | 117 | await expect( 118 | general.mint(signer.address, 0, proof, { 119 | from: signer.address, 120 | value: ethers.utils.parseEther("0.1"), 121 | }) 122 | ).to.be.revertedWith("General: sent value for non-payable token ID") 123 | }) 124 | 125 | describe("Priced Token", async function () { 126 | it("Mints 1 token ID with price of .1 ETH", async function () { 127 | const [signer] = await ethers.getSigners() 128 | 129 | const resultMint = await general 130 | .connect(signer) 131 | .mint(signer.address, 2, incorrectProof, { 132 | value: ethers.utils.parseEther("0.1"), 133 | }) 134 | await resultMint.wait() 135 | expect(await general.balanceOf(signer.address, 2)).to.equal(1) 136 | }) 137 | 138 | it("Mints 1 token ID with price of .1 ETH but sends extra ETH", async function () { 139 | const [signer] = await ethers.getSigners() 140 | 141 | const resultMint = await general 142 | .connect(signer) 143 | .mint(signer.address, 2, incorrectProof, { 144 | value: ethers.utils.parseEther("0.2"), 145 | }) 146 | await resultMint.wait() 147 | const amount = await general.balanceOf(signer.address, 2) 148 | expect(amount).to.equal(1) 149 | }) 150 | 151 | it("Fails to mint 1 token ID with incorrect price", async function () { 152 | const [signer] = await ethers.getSigners() 153 | 154 | await expect( 155 | general.mint(signer.address, 2, incorrectProof, { 156 | value: ethers.utils.parseEther("0.01"), 157 | }) 158 | ).to.be.revertedWith("General: incorrect price or not approved") 159 | }) 160 | it("Fails to mint 1 token ID with correct price but minting disabled", async function () { 161 | const [signer] = await ethers.getSigners() 162 | await general.setCanMint(false) 163 | 164 | await expect( 165 | general.mint(signer.address, 2, incorrectProof, { 166 | value: ethers.utils.parseEther("0.1"), 167 | }) 168 | ).to.be.revertedWith("General: minting is disabled") 169 | }) 170 | it("Fails to mint one token with no price set", async function () { 171 | const [signer] = await ethers.getSigners() 172 | 173 | await expect( 174 | general.mint(signer.address, 2, incorrectProof) 175 | ).to.be.revertedWith("General: incorrect price or not approved") 176 | }) 177 | 178 | it("Bypasses price by being whitelisted", async function () { 179 | const [signer] = await ethers.getSigners() 180 | 181 | const resultMint = await general 182 | .connect(signer) 183 | .mint(signer.address, 2, proof) 184 | await resultMint.wait() 185 | expect(await general.balanceOf(signer.address, 2)).to.equal(1) 186 | }) 187 | it("Successfully withdraws ETH from purchased tokens", async function () { 188 | const [signer, taker] = await ethers.getSigners() 189 | const curBal = await ethers.provider.getBalance(taker.address) 190 | 191 | const resultMint = await general 192 | .connect(signer) 193 | .mint(signer.address, 2, incorrectProof, { 194 | value: ethers.utils.parseEther("0.1"), 195 | }) 196 | await resultMint.wait() 197 | const amount = await general.balanceOf(signer.address, 2) 198 | expect(amount).to.equal(1) 199 | await general.withdraw(0, taker.address) 200 | const newBal = await ethers.provider.getBalance(taker.address) 201 | expect(newBal.sub(curBal).gte(ethers.utils.parseEther("0.1"))).to.be.true 202 | }) 203 | it("Unsuccessfully withdraws ETH from purchased tokens because not owner", async function () { 204 | const [signer, taker] = await ethers.getSigners() 205 | 206 | const resultMint = await general 207 | .connect(signer) 208 | .mint(signer.address, 2, incorrectProof, { 209 | value: ethers.utils.parseEther("0.1"), 210 | }) 211 | await resultMint.wait() 212 | const amount = await general.balanceOf(signer.address, 2) 213 | expect(amount).to.equal(1) 214 | await expect( 215 | general.connect(taker).withdraw(0, taker.address) 216 | ).to.be.revertedWith("Ownable: caller is not the owner") 217 | }) 218 | }) 219 | }) 220 | -------------------------------------------------------------------------------- /test/Invite.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai") 2 | const { ethers } = require("hardhat") 3 | 4 | describe("Invite1155", function () { 5 | let Invite 6 | let invite 7 | let TestNFT 8 | let testNFT 9 | beforeEach(async function () { 10 | Invite = await ethers.getContractFactory("Invite1155") 11 | invite = await Invite.deploy("asdasd", "AS") 12 | 13 | TestNFT = await ethers.getContractFactory("TestNFT") 14 | testNFT = await TestNFT.deploy("TestNFT", "TST") 15 | invite.createType(0, "URI 0", 0, 100) 16 | invite.createType(1, "URI 1", 0, 100) 17 | invite.createType(2, "URI 2", 0, 100) 18 | invite.createType(3, "URI 3", 0, 1) 19 | invite.createType(4, "URI 4", 0, 100) 20 | invite.createType(5, "URI 5", 0, 100) 21 | invite.createType( 22 | 6, 23 | "URI 6", 24 | ethers.BigNumber.from("100000000000000000"), 25 | 500 26 | ) 27 | invite.createType(7, "URI 7", 0, 500) 28 | invite.setWhitelistCheck("ERC721", testNFT.address, 7) 29 | invite.createType(8, "URI 8", 0, 500) 30 | invite.setCanMint(true) 31 | }) 32 | 33 | it("Mints 3 invites for token ID 0", async function () { 34 | const signers = await ethers.getSigners() 35 | let addrs = [signers[0].address, signers[1].address, signers[2].address] 36 | await invite.mintToMany(addrs, 0) 37 | const amount = await invite.balanceOf(signers[0].address, 0) 38 | expect(amount).to.equal(1) 39 | }) 40 | 41 | it("Fails to mint 2 invites of the same type to the same address", async function () { 42 | const signers = await ethers.getSigners() 43 | 44 | let addrs = [signers[0].address, signers[0].address] 45 | await expect(invite.mintToMany(addrs, 0)).to.be.revertedWith( 46 | "Invite: cannot own more than one of an Invite" 47 | ) 48 | }) 49 | it("Fails to mint 2 invites because total supply is 1", async function () { 50 | const signers = await ethers.getSigners() 51 | 52 | let addrs = [signers[0].address, signers[1].address] 53 | await expect(invite.mintToMany(addrs, 3)).to.be.revertedWith( 54 | "Invite: total supply used up" 55 | ) 56 | }) 57 | it("Fails to mint because was sent invite", async function () { 58 | const signers = await ethers.getSigners() 59 | 60 | let addrs = [signers[0].address, signers[1].address] 61 | await invite.mintToMany(addrs, 0) 62 | await invite 63 | .connect(signers[0]) 64 | .safeTransferFrom( 65 | signers[0].address, 66 | signers[2].address, 67 | 0, 68 | 1, 69 | ethers.utils.toUtf8Bytes("") 70 | ) 71 | await expect(invite.mintToMany([signers[2].address], 0)).to.be.revertedWith( 72 | "Invite: cannot own more than one of an Invite" 73 | ) 74 | }) 75 | it("Fails to withdraw because no balance", async function () { 76 | const [signer] = await ethers.getSigners() 77 | await expect(invite.withdraw(100, signer.address)).to.be.revertedWith( 78 | "Invite: not enough balance" 79 | ) 80 | }) 81 | it("Fails to setCanMint because not owner", async function () { 82 | const [_, next] = await ethers.getSigners() 83 | await expect(invite.connect(next).setCanMint(true)).to.be.revertedWith( 84 | "Ownable: caller is not the owner" 85 | ) 86 | }) 87 | it("Fails to setMintApproval because not owner", async function () { 88 | const [signer, next] = await ethers.getSigners() 89 | await expect( 90 | invite.connect(next).setMintApproval(signer.address, true, 0) 91 | ).to.be.revertedWith("Ownable: caller is not the owner") 92 | }) 93 | it("Fails to mintToMany because not owner", async function () { 94 | const [signer, next] = await ethers.getSigners() 95 | await expect( 96 | invite.connect(next).mintToMany([signer.address], 0) 97 | ).to.be.revertedWith("Ownable: caller is not the owner") 98 | }) 99 | 100 | it("Approved address fails to mint token by sending price to non-priced token", async function () { 101 | const [signer] = await ethers.getSigners() 102 | await invite.setMintApproval(signer.address, true, 0) 103 | await expect( 104 | invite.mint(signer.address, 0, { 105 | from: signer.address, 106 | value: ethers.utils.parseEther("0.1"), 107 | }) 108 | ).to.be.revertedWith("Invite: sent value for non-payable token ID") 109 | }) 110 | 111 | describe("Priced Token", async function () { 112 | it("Mints 1 token ID with price of .1 ETH", async function () { 113 | const [signer] = await ethers.getSigners() 114 | 115 | await invite.mint(signer.address, 6, { 116 | from: signer.address, 117 | value: ethers.utils.parseEther("0.1"), 118 | }) 119 | const amount = await invite.balanceOf(signer.address, 6) 120 | expect(amount).to.equal(1) 121 | }) 122 | 123 | it("Mints 1 token ID with price of .1 ETH but sends extra ETH", async function () { 124 | const [signer] = await ethers.getSigners() 125 | 126 | await invite.mint(signer.address, 6, { 127 | from: signer.address, 128 | value: ethers.utils.parseEther("0.2"), 129 | }) 130 | const amount = await invite.balanceOf(signer.address, 6) 131 | expect(amount).to.equal(1) 132 | }) 133 | 134 | it("Fails to mint 1 token ID with incorrect price", async function () { 135 | const [signer] = await ethers.getSigners() 136 | 137 | await expect( 138 | invite.mint(signer.address, 6, { 139 | from: signer.address, 140 | value: ethers.utils.parseEther("0.01"), 141 | }) 142 | ).to.be.revertedWith( 143 | "Invite: not whitelisted and msg.value is not correct price" 144 | ) 145 | }) 146 | it("Fails to mint 1 token ID with correct price but minting disabled", async function () { 147 | const [signer] = await ethers.getSigners() 148 | await invite.setCanMint(false) 149 | await expect( 150 | invite.mint(signer.address, 6, { 151 | from: signer.address, 152 | value: ethers.utils.parseEther("0.1"), 153 | }) 154 | ).to.be.revertedWith("Invite: minting is disabled") 155 | }) 156 | it("Fails to mint one token with no price set", async function () { 157 | const [signer] = await ethers.getSigners() 158 | await expect( 159 | invite.mint(signer.address, 6, { 160 | from: signer.address, 161 | }) 162 | ).to.be.revertedWith( 163 | "Invite: not whitelisted and msg.value is not correct price" 164 | ) 165 | }) 166 | 167 | it("Bypasses price by being whitelisted", async function () { 168 | const [signer] = await ethers.getSigners() 169 | 170 | await invite.setMintApproval(signer.address, true, 6) 171 | 172 | await invite.mint(signer.address, 6, { 173 | from: signer.address, 174 | }) 175 | const amount = await invite.balanceOf(signer.address, 6) 176 | expect(amount).to.equal(1) 177 | }) 178 | it("Successfully withdraws ETH from purchased tokens", async function () { 179 | const [signer, taker] = await ethers.getSigners() 180 | const curBal = await ethers.provider.getBalance(signer.address) 181 | await invite.mint(signer.address, 6, { 182 | from: signer.address, 183 | value: ethers.utils.parseEther("0.1"), 184 | }) 185 | const amount = await invite.balanceOf(signer.address, 6) 186 | expect(amount).to.equal(1) 187 | await invite.withdraw(0, taker.address) 188 | const newBal = await ethers.provider.getBalance(taker.address) 189 | expect(newBal.sub(curBal).gte(ethers.utils.parseEther("0.1"))).to.be.true 190 | }) 191 | it("Unsuccessfully withdraws ETH from purchased tokens because not owner", async function () { 192 | const [signer, taker] = await ethers.getSigners() 193 | await invite.mint(signer.address, 6, { 194 | from: signer.address, 195 | value: ethers.utils.parseEther("0.1"), 196 | }) 197 | const amount = await invite.balanceOf(signer.address, 6) 198 | expect(amount).to.equal(1) 199 | await expect( 200 | invite.connect(taker).withdraw(0, taker.address) 201 | ).to.be.revertedWith("Ownable: caller is not the owner") 202 | }) 203 | }) 204 | 205 | describe("Token Whitelisted Token", async function () { 206 | it("Mints one token for address with whitelisted token", async function () { 207 | const [signer] = await ethers.getSigners() 208 | await testNFT.mint(signer.address, 0) 209 | 210 | await invite.mint(signer.address, 7, { 211 | from: signer.address, 212 | }) 213 | const amount = await invite.balanceOf(signer.address, 7) 214 | expect(amount).to.equal(1) 215 | }) 216 | 217 | it("Fails to mints one token for address without whitelisted token", async function () { 218 | const [signer] = await ethers.getSigners() 219 | await expect( 220 | invite.mint(signer.address, 7, { 221 | from: signer.address, 222 | }) 223 | ).to.be.revertedWith("Invite: not approved to mint") 224 | }) 225 | 226 | it("Bypasses token whitelist by being directly approved", async function () { 227 | const [signer] = await ethers.getSigners() 228 | invite.setMintApproval(signer.address, true, 7) 229 | await invite.mint(signer.address, 7, { 230 | from: signer.address, 231 | }) 232 | const amount = await invite.balanceOf(signer.address, 7) 233 | expect(amount).to.equal(1) 234 | }) 235 | }) 236 | }) 237 | --------------------------------------------------------------------------------