├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.yaml ├── .gitattributes ├── .gitignore ├── .gitmodules ├── .mocharc.json ├── .npmpackagejsonlintrc.json ├── .prettierignore ├── .prettierrc ├── .solcover.js ├── .solhint.json ├── .solhintignore ├── CHANGELOG.md ├── README.md ├── addresses.js ├── contracts ├── FETH.sol ├── FNDMiddleware.sol ├── FoundationTreasury.sol ├── NFTCollection.sol ├── NFTCollectionFactory.sol ├── NFTDropCollection.sol ├── NFTDropMarket.sol ├── NFTMarket.sol ├── PercentSplitETH.sol ├── interfaces │ ├── dependencies │ │ ├── ens │ │ │ ├── IENS.sol │ │ │ ├── IFIFSRegistrar.sol │ │ │ ├── IPublicResolver.sol │ │ │ └── IReverseRegistrar.sol │ │ └── tokens │ │ │ ├── IERC20Approve.sol │ │ │ ├── IERC20IncreaseAllowance.sol │ │ │ └── IWETH9.sol │ ├── internal │ │ ├── IFethMarket.sol │ │ ├── INFTCollectionInitializer.sol │ │ ├── INFTDropCollectionInitializer.sol │ │ ├── INFTDropCollectionMint.sol │ │ └── roles │ │ │ ├── IAdminRole.sol │ │ │ ├── IHasRolesContract.sol │ │ │ ├── IOperatorRole.sol │ │ │ └── IRoles.sol │ ├── standards │ │ └── royalties │ │ │ ├── IGetFees.sol │ │ │ ├── IGetRoyalties.sol │ │ │ ├── IOwnable.sol │ │ │ ├── IRoyaltyInfo.sol │ │ │ └── ITokenCreator.sol │ └── subgraph │ │ └── IMarketDeprecatedAPIs.sol ├── libraries │ ├── AddressLibrary.sol │ ├── ArrayLibrary.sol │ ├── BytesLibrary.sol │ ├── LockedBalance.sol │ └── OZERC165Checker.sol └── mixins │ ├── collections │ ├── CollectionRoyalties.sol │ └── SequentialMintCollection.sol │ ├── nftDropMarket │ ├── NFTDropMarketCore.sol │ └── NFTDropMarketFixedPriceSale.sol │ ├── nftMarket │ ├── NFTMarketAuction.sol │ ├── NFTMarketBuyPrice.sol │ ├── NFTMarketCore.sol │ ├── NFTMarketOffer.sol │ ├── NFTMarketPrivateSaleGap.sol │ └── NFTMarketReserveAuction.sol │ ├── roles │ ├── AdminRole.sol │ └── MinterRole.sol │ ├── shared │ ├── Constants.sol │ ├── ContractFactory.sol │ ├── FETHNode.sol │ ├── FoundationTreasuryNode.sol │ ├── Gap10000.sol │ ├── MarketFees.sol │ ├── MarketSharedCore.sol │ └── SendValueWithFallbackWithdraw.sol │ └── treasury │ ├── AdminRoleEnumerable.sol │ ├── CollateralManagement.sol │ ├── OZAccessControlUpgradeable.sol │ └── OperatorRoleEnumerable.sol ├── docs ├── FETH.md ├── FNDMiddleware.md ├── FoundationTreasury.md ├── NFTCollection.md ├── NFTCollectionFactory.md ├── NFTDropCollection.md ├── NFTDropMarket.md ├── NFTMarket.md └── PercentSplitETH.md ├── docs_template.sqrl ├── hardhat.config.ts ├── package.json ├── subgraph ├── codegen.yml ├── scalars.graphql ├── schema.graphql ├── src │ ├── mappings │ │ ├── collections.ts │ │ ├── feth.ts │ │ ├── nft.ts │ │ ├── nftDropMarket.ts │ │ ├── nftMarket.ts │ │ └── percentSplit.ts │ └── shared │ │ ├── accounts.ts │ │ ├── buyNow.ts │ │ ├── constants.ts │ │ ├── conversions.ts │ │ ├── creators.ts │ │ ├── events.ts │ │ ├── feth.ts │ │ ├── ids.ts │ │ ├── offers.ts │ │ └── revenue.ts └── subgraph.template.yaml ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | wait-for: cobli/wait-for@0.0.2 4 | jobs: 5 | build: 6 | docker: 7 | - image: circleci/node:16 8 | working_directory: ~/repo 9 | steps: 10 | - checkout 11 | 12 | # NPM auth from https://discuss.circleci.com/t/yarn-with-private-packages/10157 13 | - run: echo "//registry.npmjs.org/:_authToken=${npm_TOKEN}" > ~/.npmrc 14 | - run: yarn 15 | - run: yarn build 16 | 17 | - persist_to_workspace: 18 | root: ~/repo 19 | paths: . 20 | lint: 21 | docker: 22 | - image: circleci/node:16 23 | working_directory: ~/repo 24 | steps: 25 | - attach_workspace: 26 | at: ~/repo 27 | 28 | - run: yarn lint 29 | publish: 30 | docker: 31 | - image: circleci/node:16 32 | working_directory: ~/repo 33 | steps: 34 | - attach_workspace: 35 | at: ~/repo 36 | - run: 37 | name: Authenticate with registry 38 | command: echo "//registry.npmjs.org/:_authToken=$npm_TOKEN" > ~/repo/.npmrc 39 | - run: 40 | name: Publish package 41 | command: npm publish 42 | 43 | workflows: 44 | build: 45 | jobs: 46 | - build: 47 | filters: 48 | tags: 49 | only: /.*/ 50 | branches: 51 | ignore: 52 | - gh-pages 53 | - artifacts 54 | - lint: 55 | requires: 56 | - build 57 | - publish: 58 | requires: 59 | - build 60 | filters: 61 | tags: 62 | only: /^v.*/ 63 | branches: 64 | ignore: /.*/ 65 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts/ 3 | build/ 4 | cache/ 5 | coverage/ 6 | dist/ 7 | docs/ 8 | lib/ 9 | node_modules/ 10 | src/typechain/ 11 | subgraph/config/ 12 | subgraph/build/ 13 | subgraph/generated/ 14 | subgraph/graph-node/ 15 | versions/ 16 | deployments/ 17 | 18 | # files 19 | .solcover.js 20 | coverage.json 21 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - plugin:@typescript-eslint/eslint-recommended 3 | parser: "@typescript-eslint/parser" 4 | parserOptions: 5 | project: tsconfig.json 6 | plugins: 7 | - "@typescript-eslint" 8 | - "no-only-tests" 9 | - "simple-import-sort" 10 | - "unused-imports" 11 | root: true 12 | rules: 13 | "@typescript-eslint/no-floating-promises": "error" 14 | "@typescript-eslint/no-unused-vars": "off" # Replaced by the unused-imports plugin 15 | "unused-imports/no-unused-imports": "warn" 16 | "unused-imports/no-unused-vars": "warn" 17 | "no-only-tests/no-only-tests": "warn" 18 | "simple-import-sort/imports": "warn" 19 | "simple-import-sort/exports": "warn" 20 | overrides: 21 | - files: subgraph/**/*.ts 22 | rules: 23 | prefer-const: 0 24 | "@typescript-eslint/ban-types": 0 25 | "unused-imports/no-unused-vars": 0 26 | 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain-types 6 | typechain 7 | contracts-exposed 8 | 9 | #Hardhat files 10 | cache 11 | artifacts 12 | 13 | yarn-error.log 14 | 15 | .openzeppelin/ 16 | subgraph/build 17 | subgraph/generated 18 | subgraph/subgraph.yaml 19 | 20 | .DS_Store 21 | 22 | subgraph/graph-node/ 23 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "recursive": "test", 4 | "require": "ts-node/register/files", 5 | "timeout": 60000 6 | } 7 | -------------------------------------------------------------------------------- /.npmpackagejsonlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "npm-package-json-lint-config-default", 3 | "rules": { 4 | "require-name": "error", 5 | "require-version": "off", 6 | "prefer-absolute-version-dependencies": "error", 7 | "prefer-alphabetical-peerDependencies": "error", 8 | "prefer-alphabetical-optionalDependencies": "error", 9 | "prefer-alphabetical-bundledDependencies": "error", 10 | "prefer-alphabetical-devDependencies": "error", 11 | "prefer-alphabetical-dependencies": "error", 12 | "version-format": "warning", 13 | "name-format": "error" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts/ 3 | build/ 4 | cache/ 5 | coverage/ 6 | dist/ 7 | docs/ 8 | lib/ 9 | node_modules/ 10 | src/typechain/ 11 | subgraph/build/ 12 | subgraph/generated/ 13 | subgraph/graph-node/ 14 | versions/ 15 | .vscode/ 16 | deployments/ 17 | contracts-exposed/ 18 | 19 | # files 20 | coverage.json 21 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "endOfLine":"auto", 5 | "printWidth": 120, 6 | "singleQuote": false, 7 | "tabWidth": 2, 8 | "trailingComma": "all", 9 | "overrides": [ 10 | { 11 | "files": "*.sol", 12 | "options": { 13 | "tabWidth": 2 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | const shell = require("shelljs"); 2 | 3 | // The environment variables are loaded in hardhat.config.ts 4 | const mnemonic = process.env.MNEMONIC; 5 | if (!mnemonic) { 6 | throw new Error("Please set your MNEMONIC in a .env file"); 7 | } 8 | 9 | module.exports = { 10 | istanbulReporter: ["html"], 11 | onCompileComplete: async function (_config) { 12 | await run("typechain"); 13 | }, 14 | onIstanbulComplete: async function (_config) { 15 | // We need to do this because solcover generates bespoke artifacts. 16 | shell.rm("-rf", "./artifacts"); 17 | shell.rm("-rf", "./src/typechain"); 18 | }, 19 | providerOptions: { 20 | mnemonic, 21 | }, 22 | skipFiles: ["mocks", "test", "FNDMiddleware", "archive"], 23 | istanbulReporter: ["lcov", "html", "text-summary"], 24 | mocha: { 25 | fgrep: "[skip-on-coverage]", 26 | invert: true, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 7], 6 | "compiler-version": ["error", "^0.8.12"], 7 | "const-name-snakecase": "off", 8 | "constructor-syntax": "error", 9 | "func-visibility": ["error", { "ignoreConstructors": true }], 10 | "max-line-length": ["error", 120], 11 | "not-rely-on-time": "off", 12 | "prettier/prettier": [ 13 | "error", 14 | { 15 | "endOfLine": "auto" 16 | } 17 | ], 18 | "reason-string": ["warn", { "maxLength": 64 }], 19 | "var-name-mixedcase": "off", 20 | "no-inline-assembly": "off" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .yarn/ 3 | build/ 4 | dist/ 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.3.0 4 | 5 | - Add Drops 6 | - [Remove admin cancel](https://github.com/f8n/fnd-contracts/pull/1973) functions from the market 7 | - Remove `makeOffer` (was deprecated in favor of `makeOfferV2`). 8 | - Introduce the `NFTDropMarket`. 9 | - Add `getSellerOf` to the markets and `getSellerOrOwnerOf` to the middleware. 10 | - Upgrade to [solc 0.8.16](https://github.com/ethereum/solidity/releases/tag/v0.8.16) 11 | - SplitsV3: Gas-optimization to reduce costs of creating splits 12 | 13 | ## 2.2.5 14 | 15 | - Upgrade to [solc 0.8.15](https://github.com/ethereum/solidity/releases/tag/v0.8.15) 16 | 17 | ## 2.2.4 18 | 19 | - Middleware [fix div by 0](https://github.com/f8n/fnd-contracts/pull/1888) when 0 royalties are requested 20 | 21 | ## 2.2.3 22 | 23 | - [Remove Private Sales](https://github.com/f8n/fnd-contracts/pull/1864) 24 | - [Try/catch `tokenCreator`](https://github.com/f8n/fnd-contracts/pull/1867) so that other royalty APIs are checked for contracts with a fallback function. 25 | - [Ignore `owner` when address(0)](https://github.com/f8n/fnd-contracts/pull/1868) 26 | 27 | ## 2.2.2 28 | 29 | - Middleware: [Fix `probeNFT`](https://github.com/f8n/fnd-contracts/pull/1865) for fallback function in the royalty recipient. 30 | - Upgrade to [solc 0.8.14](https://github.com/ethereum/solidity/releases/tag/v0.8.14) 31 | 32 | ## 2.2.1 33 | 34 | ### Market 35 | 36 | - [Try try catch](https://github.com/f8n/fnd-contracts/pull/1838) so that contracts with a fallback function or unsupported return types do not cause the NFT to get stuck in escrow. 37 | 38 | ## 2.2.0 39 | 40 | ### Market 41 | 42 | - [Bid Referrals](https://github.com/f8n/fnd-contracts/pull/1782): adds `placeBidV2` with referral incentives. 43 | - [Offer Referrals](https://github.com/f8n/fnd-contracts/pull/1790): adds `makeOfferV2` with referral incentives. 44 | - Auction gas savings: don't store duration/extension. https://github.com/f8n/fnd-contracts/pull/1793 45 | 46 | ## 2.1.1 47 | 48 | ### Middleware 49 | 50 | - Catch reverts in `getFees` and return `keccak256("Failed to getFees")`. 51 | 52 | ## 2.1.0 53 | 54 | ### General 55 | 56 | - Upgrade to [solc 0.8.13](https://github.com/ethereum/solidity/releases/tag/v0.8.13) 57 | 58 | ### Market 59 | 60 | - [Buy referrals](https://github.com/f8n/fnd-contracts/pull/1726): adds `buyV2` with referral incentives. 61 | - Royalties: ignore `royaltyInfo` from the NFT contract when the amount is 0 (does not impact the royalty override) 62 | - On placeBidOf, leverage FETH from an outstanding offer. 63 | - Remove withdraw from escrow (leaning on fallback to FETH instead). 64 | - Revert when setting the same buy or reserve price. 65 | - Don't check royalty APIs for non-address(0) recipient (still checks length > 0). 66 | - Use OZ ECDSA for private sale signature verification. 67 | - Gas optimizations & style improvements. 68 | 69 | ### Middleware 70 | 71 | - Switch `probeNFT` to return bytes32 instead of throwing errors. 72 | 73 | ## 2.0.3 74 | 75 | ### Middleware 76 | 77 | - `probeNFT` https://github.com/f8n/fnd-contracts/pull/1645 78 | 79 | ## 2.0.1 80 | 81 | ### Percent Split Factory 82 | 83 | - Block proxy to `increaseAllowance` 84 | - Optimize storage to save gas 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Repo — Protocol 2 | 3 | # Documentation 4 | 5 | See the [documentation](https://docs.foundation.app/docs/protocol). 6 | 7 | # Bounty Program 8 | 9 | If you find a security issue impacting contracts deployed to mainnet, please report them via our [Immunefi bounty program](https://immunefi.com/bounty/foundation/). 10 | 11 | Other issues can be reported via [GitHub Issues](https://github.com/f8n/fnd-protocol/issues). Questions? Ask in [Discussions](https://github.com/f8n/fnd-protocol/discussions). 12 | 13 | # Testing 14 | 15 | ``` 16 | yarn 17 | yarn build 18 | yarn test 19 | ``` 20 | 21 | Note that our full test suite is not included here at this time. The tests are meant to demonstrate how to work with our contracts and a few of the core features. 22 | 23 | # Code conventions 24 | 25 | ## Data sizes 26 | 27 | Where possible we should be consistent on the size of fields used. Even if it offers no immediate benefit, it will leave room for packing new fields in the future. 28 | 29 | - When uints are used as a mapping key, there is no known benefit to compressing so uint256 is preferred. 30 | - External APIs and events should always use uint256. Benefits of compressing are small and 256 is industry standard so integration should be easier. 31 | - Math in Solidity always operates in 256 bit form, so best to cast to the smaller size only at the time of storage. 32 | - Solidity does not check for overflows when down casting, explicit checks should be added when assumptions are made about user inputs. 33 | 34 | Recommendations: 35 | 36 | - ETH: uint96 37 | - Circulating supply is currently 119,440,269 ETH (119440269000000000000000000 wei / 1.2 \* 10^26). 38 | - Max uint96 is 7.9 \* 10^28. 39 | - Therefore any value capped by msg.value should never overflow uint96 assuming ETH total supply remains under 70,000,000,000 ETH. 40 | - There is currently ~2% inflation in ETH total supply (which fluctuates) and this value is expected to do down. We expect Ether will become deflationary before the supply reaches more than 500x current values. 41 | - 96 bits packs perfectly into a single slot with an address. 42 | - Dates: uint32 43 | - Current date in seconds is 1643973923 (1.6 \* 10^9). 44 | - Max uint32 is 4 \* 10^9. 45 | - Dates will not overflow uint32 until 2104. 46 | - To ensure we don't behave unexpectedly in the future, we should require dates are <= max uint32. 47 | - uint40 would allow enough space for any date, but it's an awkward size to pack with. 48 | - Sequence ID indexes: uint32 49 | - Our max sequence ID today is 149,819 auctions. 50 | - Max uint32 is 4 \* 10^9. 51 | - Indexes will not overflow uint32 until we see >28,000x growth. This is the equiv to ~300 per block for every block in Ethereum to date. 52 | - Basis points: uint16 53 | - Numbers which are by definition lower than the max uint value can be compressed appropriately. 54 | - Basis points is <= 10,000 which fits into uint16 (max of 65,536) 55 | -------------------------------------------------------------------------------- /addresses.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | module.exports = { 3 | prod: { 4 | 1: { 5 | multisig: "0x9d9C46aCa6a2c5FF6824A92d521b6381f9f8F1a9", 6 | proxyAdminAddress: "0x72DE36c8ebEAcB6100C36249552e35fefF0EE099", 7 | treasury: "0x67Df244584b67E8C51B10aD610aAfFa9a402FdB6", 8 | nft721: "0x3B3ee1931Dc30C1957379FAc9aba94D1C48a5405", 9 | nftMarket: "0xcDA72070E455bb31C7690a170224Ce43623d0B6f", 10 | percentSplit: "0x00Fc4bB5349F1104D3252c36C308688651e3eb1F", 11 | proxy: "0x461AA494426e04fB2449068BaD5948a0d720aEf2", 12 | nftCollectionFactoryV1: "0x3B612a5B49e025a6e4bA4eE4FB1EF46D13588059", 13 | feth: "0x49128CF8ABE9071ee24540a296b5DED3F9D50443", 14 | middleware: "0xC0032194d7B298465D558ab1B91Ab2ca6991959C", 15 | nftDropMarket: "0x53F451165Ba6fdbe39A134673d13948261B2334A", 16 | nftCollectionFactoryV2: "0x612E2DadDc89d91409e40f946f9f7CfE422e777E", 17 | }, 18 | }, 19 | staging: { 20 | 3: { 21 | multisig: "0xFa73B9eEE034225A8b3C6F5ecd35b625406F1385", 22 | proxyAdminAddress: "0x9aCe89b0C3b310eC4B47406195706D9C89c8e916", 23 | treasury: "0x32dAa472Ed66Ebbd14407783B34DD269ea20d664", 24 | nft721: "0x020eDedBE9F6A825D5548bdeF1266d9c77F4f58A", 25 | nftMarket: "0x7b503e206dB34148aD77e00afE214034EDF9E3fF", 26 | }, 27 | 4: { 28 | multisig: "0x18285ACA38F68d29dA62B0EB0A810789cD0D9F5C", 29 | proxyAdminAddress: "0x11A6e3C7c4FDb2f9bB5870f259bda80F963De7Aa", 30 | treasury: "0x4C6ca976Dec660738502870a9A8F9c01340D37B0", 31 | nft721: "0xB2eEa20701c9EB673aBF9205209dE2e6c2f00959", 32 | nftMarket: "0x21b700d637551f15078E11871a3c0dcCf283D1e7", 33 | }, 34 | 5: { 35 | multisig: "0xd80733B381869a0C63DB3Ca132eF52221738C440", 36 | proxyAdminAddress: "0x1cED484fF01ed99Cf08aaF9Ad916417c951Ac2c3", 37 | treasury: "0x3c5cE17ae82f39977C3DBF56433Dc8c0D9B5F2d7", 38 | nft721: "0x44458837ac4036337e5Ce46Ce28A744e05e02016", 39 | nftMarket: "0xeB1bD095061bbDb1aD065524628812cae63e4222", 40 | percentSplit: "0xCca271c0a163FD398DE7b86fB6e8f32054301C5f", 41 | proxy: "0x462B9095Ecd465BA35CecBc08930b7445919d2D1", 42 | nftCollectionFactoryV1: "0x889C3635E09B0a2d1603690288c1784155cf2CA6", 43 | feth: "0xf73Ad14f849cd9a0a1fc14Ad1A01650a9FDdE79F", 44 | middleware: "0x3436838C494fb6D8Ee9d5B97cFf0E54af5447EB3", 45 | nftCollectionFactoryV2: "0x8b9788Fd08496e791c066c12021F25a8Eb20CC0b", 46 | nftDropMarket: "0xe43562f11737443F760dBf885fa0D30c45C6927B", 47 | }, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /contracts/FoundationTreasury.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ・ 3 | * ★ 4 | ・ 。 5 |  ・ ゚☆ 。 6 |     * ★ ゚・。 * 。 7 |   * ☆ 。・゚*.。 8 |    ゚ *.。☆。★ ・ 9 | *  ゚。·*・。 ゚* 10 |    ☆゚・。°*. ゚ 11 |   ・ ゚*。・゚★。 12 |   ・ *゚。   * 13 |  ・゚*。★・ 14 | ☆∴。 * 15 |     * ★ 。 16 | ` .-:::::-.` `-::---...``` 17 | `-:` .:+ssssoooo++//:.` .-/+shhhhhhhhhhhhhyyyssooo: 18 | .--::. .+ossso+/////++/:://-` .////+shhhhhhhhhhhhhhhhhhhhhy 19 | `-----::. `/+////+++///+++/:--:/+/- -////+shhhhhhhhhhhhhhhhhhhhhy 20 | `------:::-` `//-.``.-/+ooosso+:-.-/oso- -////+shhhhhhhhhhhhhhhhhhhhhy 21 | .--------:::-` :+:.` .-/osyyyyyyso++syhyo.-////+shhhhhhhhhhhhhhhhhhhhhy 22 | `-----------:::-. +o+:-.-:/oyhhhhhhdhhhhhdddy:-////+shhhhhhhhhhhhhhhhhhhhhy 23 | .------------::::-- `oys+/::/+shhhhhhhdddddddddy/-////+shhhhhhhhhhhhhhhhhhhhhy 24 | .--------------:::::-` +ys+////+yhhhhhhhddddddddhy:-////+yhhhhhhhhhhhhhhhhhhhhhy 25 | `----------------::::::-`.ss+/:::+oyhhhhhhhhhhhhhhho`-////+shhhhhhhhhhhhhhhhhhhhhy 26 | .------------------:::::::.-so//::/+osyyyhhhhhhhhhys` -////+shhhhhhhhhhhhhhhhhhhhhy 27 | `.-------------------::/:::::..+o+////+oosssyyyyyyys+` .////+shhhhhhhhhhhhhhhhhhhhhy 28 | .--------------------::/:::.` -+o++++++oooosssss/. `-//+shhhhhhhhhhhhhhhhhhhhyo 29 | .------- ``````.......--` `-/+ooooosso+/-` `./++++///:::--...``hhhhyo 30 | ````` 31 | *  32 | ・ 。 33 |     ・  ゚☆ 。 34 |     * ★ ゚・。 * 。 35 |   * ☆ 。・゚*.。 36 |    ゚ *.。☆。★ ・ 37 | *  ゚。·*・。 ゚* 38 |    ☆゚・。°*. ゚ 39 |   ・ ゚*。・゚★。 40 |   ・ *゚。   * 41 |  ・゚*。★・ 42 | ☆∴。 * 43 | ・ 。 44 | */ 45 | 46 | // SPDX-License-Identifier: MIT OR Apache-2.0 47 | 48 | pragma solidity ^0.8.12; 49 | 50 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 51 | 52 | import "./mixins/treasury/AdminRoleEnumerable.sol"; 53 | import "./mixins/treasury/CollateralManagement.sol"; 54 | import "./mixins/treasury/OperatorRoleEnumerable.sol"; 55 | 56 | /** 57 | * @title Manage revenue and roles for Foundation. 58 | * @notice All fees generated by the market are forwarded to this contract. 59 | * It also defines the Admin and Operator roles which are used in other contracts. 60 | * @author batu-inal & HardlyDifficult 61 | */ 62 | contract FoundationTreasury is Initializable, AdminRoleEnumerable, OperatorRoleEnumerable, CollateralManagement { 63 | // solhint-disable-next-line no-empty-blocks 64 | constructor() initializer { 65 | // Initialize the template to avoid confusion. 66 | // https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680/6 67 | } 68 | 69 | /** 70 | * @notice Called one time after deployment to initialize the contract. 71 | * @param admin The account to add as the initial admin. 72 | */ 73 | function initialize(address admin) external initializer { 74 | AdminRoleEnumerable._initializeAdminRole(admin); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /contracts/NFTDropCollection.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 7 | import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; 8 | import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol"; 9 | import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; 10 | import "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; 11 | import "@openzeppelin/contracts/utils/Strings.sol"; 12 | 13 | import "./interfaces/internal/INFTDropCollectionInitializer.sol"; 14 | import "./interfaces/internal/INFTDropCollectionMint.sol"; 15 | 16 | import "./mixins/collections/CollectionRoyalties.sol"; 17 | import "./mixins/collections/SequentialMintCollection.sol"; 18 | import "./mixins/roles/AdminRole.sol"; 19 | import "./mixins/roles/MinterRole.sol"; 20 | import "./mixins/shared/ContractFactory.sol"; 21 | 22 | /** 23 | * @title A contract to batch mint a collection of NFTs. 24 | * @notice A 10% royalty to the creator is included which may be split with collaborators. 25 | * @dev A collection can have up to 4,294,967,295 (2^32-1) tokens 26 | * @author batu-inal & HardlyDifficult 27 | */ 28 | contract NFTDropCollection is 29 | INFTDropCollectionInitializer, 30 | INFTDropCollectionMint, 31 | IGetRoyalties, 32 | IGetFees, 33 | IRoyaltyInfo, 34 | ITokenCreator, 35 | ContractFactory, 36 | Initializable, 37 | ContextUpgradeable, 38 | ERC165Upgradeable, 39 | AccessControlUpgradeable, 40 | AdminRole, 41 | MinterRole, 42 | ERC721Upgradeable, 43 | ERC721BurnableUpgradeable, 44 | SequentialMintCollection, 45 | CollectionRoyalties 46 | { 47 | using Strings for uint256; 48 | 49 | /****** Slot 0 (after inheritance) ******/ 50 | /** 51 | * @notice The address to pay the proceeds/royalties for the collection. 52 | * @dev If this is set to address(0) then the proceeds go to the creator. 53 | */ 54 | address payable private paymentAddress; 55 | /** 56 | * @notice Whether the collection is revealed or not. 57 | */ 58 | bool public isRevealed; 59 | // 88 bits free space 60 | 61 | /****** Slot 1 ******/ 62 | /** 63 | * @notice The base URI used for all NFTs in this collection. 64 | * @dev The `.json` is appended to this to obtain an NFT's `tokenURI`. 65 | * e.g. The URI for `tokenId`: "1" with `baseURI`: "ipfs://foo/" is "ipfs://foo/1.json". 66 | * @return The base URI used by this collection. 67 | */ 68 | string public baseURI; 69 | 70 | /****** End of storage ******/ 71 | 72 | /** 73 | * @notice Emitted when the collection is revealed. 74 | * @param baseURI The base URI for the collection. 75 | * @param isRevealed Whether the collection is revealed. 76 | */ 77 | event URIUpdated(string baseURI, bool isRevealed); 78 | 79 | modifier validBaseURI(string calldata baseURI_) { 80 | require(bytes(baseURI_).length > 0, "NFTDropCollection: `baseURI_` must be set"); 81 | _; 82 | } 83 | 84 | modifier onlyWhileUnrevealed() { 85 | require(!isRevealed, "NFTDropCollection: Already revealed"); 86 | _; 87 | } 88 | 89 | /** 90 | * @notice Initialize the template's immutable variables. 91 | * @param _contractFactory The factory which will be used to create collection contracts. 92 | */ 93 | constructor(address _contractFactory) 94 | ContractFactory(_contractFactory) // solhint-disable-next-line no-empty-blocks 95 | {} 96 | 97 | /** 98 | * @notice Called by the contract factory on creation. 99 | * @param _creator The creator of this collection. 100 | * This account is the default admin for this collection. 101 | * @param _name The collection's `name`. 102 | * @param _symbol The collection's `symbol`. 103 | * @param baseURI_ The base URI for the collection. 104 | * @param _isRevealed Whether the collection is revealed or not. 105 | * @param _maxTokenId The max token id for this collection. 106 | * @param _approvedMinter An optional address to grant the MINTER_ROLE. 107 | * Set to address(0) if only admins should be granted permission to mint. 108 | * @param _paymentAddress The address that will receive royalties and mint payments. 109 | */ 110 | function initialize( 111 | address payable _creator, 112 | string calldata _name, 113 | string calldata _symbol, 114 | string calldata baseURI_, 115 | bool _isRevealed, 116 | uint32 _maxTokenId, 117 | address _approvedMinter, 118 | address payable _paymentAddress 119 | ) external initializer onlyContractFactory validBaseURI(baseURI_) { 120 | // Initialize the NFT 121 | __ERC721_init(_name, _symbol); 122 | _initializeSequentialMintCollection(_creator, _maxTokenId); 123 | 124 | // Initialize royalties 125 | if (_paymentAddress != address(0)) { 126 | // If no payment address was defined, `.owner` will be returned in getTokenCreatorPaymentAddress() below. 127 | paymentAddress = _paymentAddress; 128 | } 129 | 130 | // Initialize URI 131 | baseURI = baseURI_; 132 | isRevealed = _isRevealed; 133 | 134 | // Initialize access control 135 | AdminRole._initializeAdminRole(_creator); 136 | if (_approvedMinter != address(0)) { 137 | MinterRole._initializeMinterRole(_approvedMinter); 138 | } 139 | } 140 | 141 | /** 142 | * @notice Allows the collection admin to burn a specific token if they currently own the NFT. 143 | * @param tokenId The ID of the NFT to burn. 144 | * @dev The function here asserts `onlyAdmin` while the super confirms ownership. 145 | */ 146 | function burn(uint256 tokenId) public override onlyAdmin { 147 | super.burn(tokenId); 148 | } 149 | 150 | /** 151 | * @notice Mint `count` number of NFTs for the `to` address. 152 | * @dev This is only callable by an address with either the MINTER_ROLE or the DEFAULT_ADMIN_ROLE. 153 | * @param count The number of NFTs to mint. 154 | * @param to The address to mint the NFTs for. 155 | * @return firstTokenId The tokenId for the first NFT minted. 156 | * The other minted tokens are assigned sequentially, so `firstTokenId` - `firstTokenId + count - 1` were minted. 157 | */ 158 | function mintCountTo(uint16 count, address to) external onlyMinterOrAdmin returns (uint256 firstTokenId) { 159 | require(count != 0, "NFTDropCollection: `count` must be greater than 0"); 160 | 161 | unchecked { 162 | // If +1 overflows then +count would also overflow, since count > 0. 163 | firstTokenId = latestTokenId + 1; 164 | } 165 | latestTokenId = latestTokenId + count; 166 | uint256 lastTokenId = latestTokenId; 167 | require(lastTokenId <= maxTokenId, "NFTDropCollection: Exceeds max tokenId"); 168 | 169 | for (uint256 i = firstTokenId; i <= lastTokenId; ) { 170 | _safeMint(to, i); 171 | unchecked { 172 | ++i; 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * @notice Allows a collection admin to reveal the collection's final content. 179 | * @dev Once revealed, the collection's content is immutable. 180 | * Use `updatePreRevealContent` to update content while unrevealed. 181 | * @param baseURI_ The base URI of the final content for this collection. 182 | */ 183 | function reveal(string calldata baseURI_) external onlyAdmin validBaseURI(baseURI_) onlyWhileUnrevealed { 184 | isRevealed = true; 185 | 186 | // Set the new base URI. 187 | baseURI = baseURI_; 188 | emit URIUpdated(baseURI_, true); 189 | } 190 | 191 | /** 192 | * @notice Allows a collection admin to destroy this contract only if 193 | * no NFTs have been minted yet or the minted NFTs have been burned. 194 | * @dev Once destructed, a new collection could be deployed to this address (although that's discouraged). 195 | */ 196 | function selfDestruct() external onlyAdmin { 197 | _selfDestruct(); 198 | } 199 | 200 | /** 201 | * @notice Allows the owner to set a max tokenID. 202 | * This provides a guarantee to collectors about the limit of this collection contract. 203 | * @dev Once this value has been set, it may be decreased but can never be increased. 204 | * This max may be more than the final `totalSupply` if 1 or more tokens were burned. 205 | * @param _maxTokenId The max tokenId to set, all NFTs must have a tokenId less than or equal to this value. 206 | */ 207 | function updateMaxTokenId(uint32 _maxTokenId) external onlyAdmin { 208 | _updateMaxTokenId(_maxTokenId); 209 | } 210 | 211 | /** 212 | * @notice Allows a collection admin to update the pre-reveal content. 213 | * @dev Use `reveal` to reveal the final content for this collection. 214 | * @param baseURI_ The base URI of the pre-reveal content. 215 | */ 216 | function updatePreRevealContent(string calldata baseURI_) 217 | external 218 | validBaseURI(baseURI_) 219 | onlyWhileUnrevealed 220 | onlyAdmin 221 | { 222 | baseURI = baseURI_; 223 | emit URIUpdated(baseURI_, false); 224 | } 225 | 226 | function _burn(uint256 tokenId) internal override(ERC721Upgradeable, SequentialMintCollection) { 227 | super._burn(tokenId); 228 | } 229 | 230 | /** 231 | * @inheritdoc CollectionRoyalties 232 | */ 233 | function getTokenCreatorPaymentAddress( 234 | uint256 /* tokenId */ 235 | ) public view override returns (address payable creatorPaymentAddress) { 236 | creatorPaymentAddress = paymentAddress; 237 | if (creatorPaymentAddress == address(0)) { 238 | creatorPaymentAddress = owner; 239 | } 240 | } 241 | 242 | /** 243 | * @notice Get the number of tokens which can still be minted. 244 | * @return count The max number of additional NFTs that can be minted by this collection. 245 | */ 246 | function numberOfTokensAvailableToMint() external view returns (uint256 count) { 247 | // Mint ensures that latestTokenId is always <= maxTokenId 248 | unchecked { 249 | count = maxTokenId - latestTokenId; 250 | } 251 | } 252 | 253 | /** 254 | * @inheritdoc IERC165Upgradeable 255 | */ 256 | function supportsInterface(bytes4 interfaceId) 257 | public 258 | view 259 | override(ERC165Upgradeable, AccessControlUpgradeable, ERC721Upgradeable, CollectionRoyalties) 260 | returns (bool interfaceSupported) 261 | { 262 | interfaceSupported = (interfaceId == type(INFTDropCollectionMint).interfaceId || 263 | super.supportsInterface(interfaceId)); 264 | } 265 | 266 | /** 267 | * @inheritdoc IERC721MetadataUpgradeable 268 | */ 269 | function tokenURI(uint256 tokenId) public view override returns (string memory uri) { 270 | _requireMinted(tokenId); 271 | 272 | uri = string.concat(baseURI, tokenId.toString(), ".json"); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /contracts/NFTDropMarket.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ・ 3 | * ★ 4 | ・ 。 5 |  ・ ゚☆ 。 6 |     * ★ ゚・。 * 。 7 |   * ☆ 。・゚*.。 8 |    ゚ *.。☆。★ ・ 9 | ​ 10 | ` .-:::::-.` `-::---...``` 11 | `-:` .:+ssssoooo++//:.` .-/+shhhhhhhhhhhhhyyyssooo: 12 | .--::. .+ossso+/////++/:://-` .////+shhhhhhhhhhhhhhhhhhhhhy 13 | `-----::. `/+////+++///+++/:--:/+/- -////+shhhhhhhhhhhhhhhhhhhhhy 14 | `------:::-` `//-.``.-/+ooosso+:-.-/oso- -////+shhhhhhhhhhhhhhhhhhhhhy 15 | .--------:::-` :+:.` .-/osyyyyyyso++syhyo.-////+shhhhhhhhhhhhhhhhhhhhhy 16 | `-----------:::-. +o+:-.-:/oyhhhhhhdhhhhhdddy:-////+shhhhhhhhhhhhhhhhhhhhhy 17 | .------------::::-- `oys+/::/+shhhhhhhdddddddddy/-////+shhhhhhhhhhhhhhhhhhhhhy 18 | .--------------:::::-` +ys+////+yhhhhhhhddddddddhy:-////+yhhhhhhhhhhhhhhhhhhhhhy 19 | `----------------::::::-`.ss+/:::+oyhhhhhhhhhhhhhhho`-////+shhhhhhhhhhhhhhhhhhhhhy 20 | .------------------:::::::.-so//::/+osyyyhhhhhhhhhys` -////+shhhhhhhhhhhhhhhhhhhhhy 21 | `.-------------------::/:::::..+o+////+oosssyyyyyyys+` .////+shhhhhhhhhhhhhhhhhhhhhy 22 | .--------------------::/:::.` -+o++++++oooosssss/. `-//+shhhhhhhhhhhhhhhhhhhhyo 23 | .------- ``````.......--` `-/+ooooosso+/-` `./++++///:::--...``hhhhyo 24 | ````` 25 | *  26 | ・ 。 27 |     ・  ゚☆ 。 28 |     * ★ ゚・。 * 。 29 |   * ☆ 。・゚*.。 30 |    ゚ *.。☆。★ ・ 31 | *  ゚。·*・。 ゚* 32 |    ☆゚・。°*. ゚ 33 |   ・ ゚*。・゚★。 34 |   ・ *゚。   * 35 |  ・゚*。★・ 36 | ☆∴。 * 37 | ・ 。 38 | */ 39 | 40 | // SPDX-License-Identifier: MIT OR Apache-2.0 41 | 42 | pragma solidity ^0.8.12; 43 | 44 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 45 | import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; 46 | 47 | import "./mixins/shared/FETHNode.sol"; 48 | import "./mixins/shared/FoundationTreasuryNode.sol"; 49 | import "./mixins/shared/Gap10000.sol"; 50 | import "./mixins/shared/MarketFees.sol"; 51 | import "./mixins/shared/MarketSharedCore.sol"; 52 | import "./mixins/shared/SendValueWithFallbackWithdraw.sol"; 53 | 54 | import "./mixins/nftDropMarket/NFTDropMarketCore.sol"; 55 | import "./mixins/nftDropMarket/NFTDropMarketFixedPriceSale.sol"; 56 | 57 | error NFTDropMarket_NFT_Already_Minted(); 58 | 59 | /** 60 | * @title A market for minting NFTs with Foundation. 61 | * @author batu-inal & HardlyDifficult 62 | */ 63 | contract NFTDropMarket is 64 | Initializable, 65 | FoundationTreasuryNode, 66 | FETHNode, 67 | MarketSharedCore, 68 | NFTDropMarketCore, 69 | ReentrancyGuardUpgradeable, 70 | SendValueWithFallbackWithdraw, 71 | MarketFees, 72 | Gap10000, 73 | NFTDropMarketFixedPriceSale 74 | { 75 | /** 76 | * @notice Set immutable variables for the implementation contract. 77 | * @dev Using immutable instead of constants allows us to use different values on testnet. 78 | * @param treasury The Foundation Treasury contract address. 79 | * @param feth The FETH ERC-20 token contract address. 80 | * @param royaltyRegistry The Royalty Registry contract address. 81 | */ 82 | constructor( 83 | address payable treasury, 84 | address feth, 85 | address royaltyRegistry 86 | ) 87 | FoundationTreasuryNode(treasury) 88 | FETHNode(feth) 89 | MarketFees( 90 | /* protocolFeeInBasisPoints: */ 91 | 1500, 92 | royaltyRegistry, 93 | /* assumePrimarySale: */ 94 | true 95 | ) 96 | initializer // solhint-disable-next-line no-empty-blocks 97 | {} 98 | 99 | /** 100 | * @notice Called once to configure the contract after the initial proxy deployment. 101 | * @dev This farms the initialize call out to inherited contracts as needed to initialize mutable variables. 102 | */ 103 | function initialize() external initializer { 104 | ReentrancyGuardUpgradeable.__ReentrancyGuard_init(); 105 | } 106 | 107 | /** 108 | * @inheritdoc MarketSharedCore 109 | * @dev Returns address(0) if the NFT has already been sold, otherwise checks for a listing in this market. 110 | */ 111 | function _getSellerOf(address nftContract, uint256 tokenId) 112 | internal 113 | view 114 | override(MarketSharedCore, NFTDropMarketFixedPriceSale) 115 | returns (address payable seller) 116 | { 117 | // Check the current owner first in case it has been sold. 118 | try IERC721(nftContract).ownerOf(tokenId) returns (address owner) { 119 | if (owner != address(0)) { 120 | // If sold, return address(0) since that owner cannot sell via this market. 121 | return payable(address(0)); 122 | } 123 | } catch // solhint-disable-next-line no-empty-blocks 124 | { 125 | // Fall through 126 | } 127 | 128 | return super._getSellerOf(nftContract, tokenId); 129 | } 130 | 131 | /** 132 | * @inheritdoc MarketSharedCore 133 | * @dev Reverts if the NFT has already been sold, otherwise checks for a listing in this market. 134 | */ 135 | function _getSellerOrOwnerOf(address nftContract, uint256 tokenId) 136 | internal 137 | view 138 | override 139 | returns (address payable sellerOrOwner) 140 | { 141 | // Check the current owner first in case it has been sold. 142 | try IERC721(nftContract).ownerOf(tokenId) returns (address owner) { 143 | if (owner != address(0)) { 144 | // Once an NFT has been minted, it cannot be sold through this contract. 145 | revert NFTDropMarket_NFT_Already_Minted(); 146 | } 147 | } catch // solhint-disable-next-line no-empty-blocks 148 | { 149 | // Fall through 150 | } 151 | 152 | sellerOrOwner = super._getSellerOf(nftContract, tokenId); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /contracts/NFTMarket.sol: -------------------------------------------------------------------------------- 1 | /* 2 | ・ 3 | * ★ 4 | ・ 。 5 |  ・ ゚☆ 。 6 |     * ★ ゚・。 * 。 7 |   * ☆ 。・゚*.。 8 |    ゚ *.。☆。★ ・ 9 | ​ 10 | ` .-:::::-.` `-::---...``` 11 | `-:` .:+ssssoooo++//:.` .-/+shhhhhhhhhhhhhyyyssooo: 12 | .--::. .+ossso+/////++/:://-` .////+shhhhhhhhhhhhhhhhhhhhhy 13 | `-----::. `/+////+++///+++/:--:/+/- -////+shhhhhhhhhhhhhhhhhhhhhy 14 | `------:::-` `//-.``.-/+ooosso+:-.-/oso- -////+shhhhhhhhhhhhhhhhhhhhhy 15 | .--------:::-` :+:.` .-/osyyyyyyso++syhyo.-////+shhhhhhhhhhhhhhhhhhhhhy 16 | `-----------:::-. +o+:-.-:/oyhhhhhhdhhhhhdddy:-////+shhhhhhhhhhhhhhhhhhhhhy 17 | .------------::::-- `oys+/::/+shhhhhhhdddddddddy/-////+shhhhhhhhhhhhhhhhhhhhhy 18 | .--------------:::::-` +ys+////+yhhhhhhhddddddddhy:-////+yhhhhhhhhhhhhhhhhhhhhhy 19 | `----------------::::::-`.ss+/:::+oyhhhhhhhhhhhhhhho`-////+shhhhhhhhhhhhhhhhhhhhhy 20 | .------------------:::::::.-so//::/+osyyyhhhhhhhhhys` -////+shhhhhhhhhhhhhhhhhhhhhy 21 | `.-------------------::/:::::..+o+////+oosssyyyyyyys+` .////+shhhhhhhhhhhhhhhhhhhhhy 22 | .--------------------::/:::.` -+o++++++oooosssss/. `-//+shhhhhhhhhhhhhhhhhhhhyo 23 | .------- ``````.......--` `-/+ooooosso+/-` `./++++///:::--...``hhhhyo 24 | ````` 25 | *  26 | ・ 。 27 |     ・  ゚☆ 。 28 |     * ★ ゚・。 * 。 29 |   * ☆ 。・゚*.。 30 |    ゚ *.。☆。★ ・ 31 | *  ゚。·*・。 ゚* 32 |    ☆゚・。°*. ゚ 33 |   ・ ゚*。・゚★。 34 |   ・ *゚。   * 35 |  ・゚*。★・ 36 | ☆∴。 * 37 | ・ 。 38 | */ 39 | 40 | // SPDX-License-Identifier: MIT OR Apache-2.0 41 | 42 | pragma solidity ^0.8.12; 43 | 44 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 45 | import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; 46 | 47 | import "./mixins/shared/Constants.sol"; 48 | import "./mixins/shared/FETHNode.sol"; 49 | import "./mixins/shared/FoundationTreasuryNode.sol"; 50 | import "./mixins/shared/MarketFees.sol"; 51 | import "./mixins/shared/MarketSharedCore.sol"; 52 | import "./mixins/shared/SendValueWithFallbackWithdraw.sol"; 53 | 54 | import "./mixins/nftMarket/NFTMarketAuction.sol"; 55 | import "./mixins/nftMarket/NFTMarketBuyPrice.sol"; 56 | import "./mixins/nftMarket/NFTMarketCore.sol"; 57 | import "./mixins/nftMarket/NFTMarketOffer.sol"; 58 | import "./mixins/nftMarket/NFTMarketPrivateSaleGap.sol"; 59 | import "./mixins/nftMarket/NFTMarketReserveAuction.sol"; 60 | 61 | /** 62 | * @title A market for NFTs on Foundation. 63 | * @notice The Foundation marketplace is a contract which allows traders to buy and sell NFTs. 64 | * It supports buying and selling via auctions, private sales, buy price, and offers. 65 | * @dev All sales in the Foundation market will pay the creator 10% royalties on secondary sales. This is not specific 66 | * to NFTs minted on Foundation, it should work for any NFT. If royalty information was not defined when the NFT was 67 | * originally deployed, it may be added using the [Royalty Registry](https://royaltyregistry.xyz/) which will be 68 | * respected by our market contract. 69 | * @author batu-inal & HardlyDifficult 70 | */ 71 | contract NFTMarket is 72 | Initializable, 73 | FoundationTreasuryNode, 74 | FETHNode, 75 | MarketSharedCore, 76 | NFTMarketCore, 77 | ReentrancyGuardUpgradeable, 78 | SendValueWithFallbackWithdraw, 79 | MarketFees, 80 | NFTMarketAuction, 81 | NFTMarketReserveAuction, 82 | NFTMarketPrivateSaleGap, 83 | NFTMarketBuyPrice, 84 | NFTMarketOffer 85 | { 86 | /** 87 | * @notice Set immutable variables for the implementation contract. 88 | * @dev Using immutable instead of constants allows us to use different values on testnet. 89 | * @param treasury The Foundation Treasury contract address. 90 | * @param feth The FETH ERC-20 token contract address. 91 | * @param royaltyRegistry The Royalty Registry contract address. 92 | * @param duration The duration of the auction in seconds. 93 | */ 94 | constructor( 95 | address payable treasury, 96 | address feth, 97 | address royaltyRegistry, 98 | uint256 duration 99 | ) 100 | FoundationTreasuryNode(treasury) 101 | FETHNode(feth) 102 | MarketFees( 103 | /* protocolFeeInBasisPoints: */ 104 | 500, 105 | royaltyRegistry, 106 | /* assumePrimarySale: */ 107 | false 108 | ) 109 | NFTMarketReserveAuction(duration) 110 | initializer // solhint-disable-next-line no-empty-blocks 111 | {} 112 | 113 | /** 114 | * @notice Called once to configure the contract after the initial proxy deployment. 115 | * @dev This farms the initialize call out to inherited contracts as needed to initialize mutable variables. 116 | */ 117 | function initialize() external initializer { 118 | ReentrancyGuardUpgradeable.__ReentrancyGuard_init(); 119 | NFTMarketAuction._initializeNFTMarketAuction(); 120 | } 121 | 122 | /** 123 | * @inheritdoc NFTMarketCore 124 | */ 125 | function _beforeAuctionStarted(address nftContract, uint256 tokenId) 126 | internal 127 | override(NFTMarketCore, NFTMarketBuyPrice, NFTMarketOffer) 128 | { 129 | // This is a no-op function required to avoid compile errors. 130 | super._beforeAuctionStarted(nftContract, tokenId); 131 | } 132 | 133 | /** 134 | * @inheritdoc NFTMarketCore 135 | */ 136 | function _transferFromEscrow( 137 | address nftContract, 138 | uint256 tokenId, 139 | address recipient, 140 | address authorizeSeller 141 | ) internal override(NFTMarketCore, NFTMarketReserveAuction, NFTMarketBuyPrice) { 142 | // This is a no-op function required to avoid compile errors. 143 | super._transferFromEscrow(nftContract, tokenId, recipient, authorizeSeller); 144 | } 145 | 146 | /** 147 | * @inheritdoc NFTMarketCore 148 | */ 149 | function _transferFromEscrowIfAvailable( 150 | address nftContract, 151 | uint256 tokenId, 152 | address recipient 153 | ) internal override(NFTMarketCore, NFTMarketReserveAuction, NFTMarketBuyPrice) { 154 | // This is a no-op function required to avoid compile errors. 155 | super._transferFromEscrowIfAvailable(nftContract, tokenId, recipient); 156 | } 157 | 158 | /** 159 | * @inheritdoc NFTMarketCore 160 | */ 161 | function _transferToEscrow(address nftContract, uint256 tokenId) 162 | internal 163 | override(NFTMarketCore, NFTMarketReserveAuction, NFTMarketBuyPrice) 164 | { 165 | // This is a no-op function required to avoid compile errors. 166 | super._transferToEscrow(nftContract, tokenId); 167 | } 168 | 169 | /** 170 | * @inheritdoc MarketSharedCore 171 | */ 172 | function _getSellerOf(address nftContract, uint256 tokenId) 173 | internal 174 | view 175 | override(MarketSharedCore, NFTMarketCore, NFTMarketReserveAuction, NFTMarketBuyPrice) 176 | returns (address payable seller) 177 | { 178 | // This is a no-op function required to avoid compile errors. 179 | seller = super._getSellerOf(nftContract, tokenId); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /contracts/interfaces/dependencies/ens/IENS.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @title Interface for the main ENS contract 7 | */ 8 | interface IENS { 9 | function setSubnodeOwner( 10 | bytes32 node, 11 | bytes32 label, 12 | address owner 13 | ) external returns (bytes32); 14 | 15 | function setResolver(bytes32 node, address resolver) external; 16 | 17 | function owner(bytes32 node) external view returns (address); 18 | 19 | function resolver(bytes32 node) external view returns (address); 20 | } 21 | -------------------------------------------------------------------------------- /contracts/interfaces/dependencies/ens/IFIFSRegistrar.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @title Interface for the main ENS FIFSRegistrar contract. 7 | * @notice Used in testnet only. 8 | */ 9 | interface IFIFSRegistrar { 10 | function register(bytes32 label, address owner) external; 11 | } 12 | -------------------------------------------------------------------------------- /contracts/interfaces/dependencies/ens/IPublicResolver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @title Interface for the main ENS resolver contract. 7 | */ 8 | interface IPublicResolver { 9 | function setAddr(bytes32 node, address a) external; 10 | 11 | function name(bytes32 node) external view returns (string memory); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/dependencies/ens/IReverseRegistrar.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @title Interface for the main ENS reverse registrar contract. 7 | */ 8 | interface IReverseRegistrar { 9 | function setName(string memory name) external; 10 | 11 | function node(address addr) external pure returns (bytes32); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/dependencies/tokens/IERC20Approve.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | interface IERC20Approve { 6 | function approve(address spender, uint256 amount) external returns (bool); 7 | } 8 | -------------------------------------------------------------------------------- /contracts/interfaces/dependencies/tokens/IERC20IncreaseAllowance.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | interface IERC20IncreaseAllowance { 6 | function increaseAllowance(address spender, uint256 addedValue) external returns (bool); 7 | } 8 | -------------------------------------------------------------------------------- /contracts/interfaces/dependencies/tokens/IWETH9.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * Mainnet: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 3 | * Goerli: 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6 4 | */ 5 | 6 | // SPDX-License-Identifier: MIT OR Apache-2.0 7 | 8 | pragma solidity ^0.8.12; 9 | 10 | interface IWETH9 { 11 | function deposit() external payable; 12 | 13 | function transfer(address to, uint256 value) external returns (bool); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/interfaces/internal/IFethMarket.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @notice Interface for functions the market uses in FETH. 7 | * @author batu-inal & HardlyDifficult 8 | */ 9 | interface IFethMarket { 10 | function depositFor(address account) external payable; 11 | 12 | function marketLockupFor(address account, uint256 amount) external payable returns (uint256 expiration); 13 | 14 | function marketWithdrawFrom(address from, uint256 amount) external; 15 | 16 | function marketWithdrawLocked( 17 | address account, 18 | uint256 expiration, 19 | uint256 amount 20 | ) external; 21 | 22 | function marketUnlockFor( 23 | address account, 24 | uint256 expiration, 25 | uint256 amount 26 | ) external; 27 | 28 | function marketChangeLockup( 29 | address unlockFrom, 30 | uint256 unlockExpiration, 31 | uint256 unlockAmount, 32 | address lockupFor, 33 | uint256 lockupAmount 34 | ) external payable returns (uint256 expiration); 35 | } 36 | -------------------------------------------------------------------------------- /contracts/interfaces/internal/INFTCollectionInitializer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @author batu-inal & HardlyDifficult 7 | */ 8 | interface INFTCollectionInitializer { 9 | function initialize( 10 | address payable _creator, 11 | string memory _name, 12 | string memory _symbol 13 | ) external; 14 | } 15 | -------------------------------------------------------------------------------- /contracts/interfaces/internal/INFTDropCollectionInitializer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @author batu-inal & HardlyDifficult 7 | */ 8 | interface INFTDropCollectionInitializer { 9 | function initialize( 10 | address payable _creator, 11 | string calldata _name, 12 | string calldata _symbol, 13 | string calldata _baseURI, 14 | bool isRevealed, 15 | uint32 _maxTokenId, 16 | address _approvedMinter, 17 | address payable _paymentAddress 18 | ) external; 19 | } 20 | -------------------------------------------------------------------------------- /contracts/interfaces/internal/INFTDropCollectionMint.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @notice The required interface for collections to support the NFTDropMarket. 7 | * @dev This interface must be registered as a ERC165 supported interface to support the NFTDropMarket. 8 | * @author batu-inal & HardlyDifficult 9 | */ 10 | interface INFTDropCollectionMint { 11 | function mintCountTo(uint16 count, address to) external returns (uint256 firstTokenId); 12 | 13 | function numberOfTokensAvailableToMint() external view returns (uint256 count); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/interfaces/internal/roles/IAdminRole.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @notice Interface for AdminRole which wraps the default admin role from 7 | * OpenZeppelin's AccessControl for easy integration. 8 | * @author batu-inal & HardlyDifficult 9 | */ 10 | interface IAdminRole { 11 | function isAdmin(address account) external view returns (bool); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/internal/roles/IHasRolesContract.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "./IRoles.sol"; 6 | 7 | /** 8 | * @author batu-inal & HardlyDifficult 9 | */ 10 | interface IHasRolesContract { 11 | function rolesManager() external returns (IRoles); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/internal/roles/IOperatorRole.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @notice Interface for OperatorRole which wraps a role from 7 | * OpenZeppelin's AccessControl for easy integration. 8 | * @author batu-inal & HardlyDifficult 9 | */ 10 | interface IOperatorRole { 11 | function isOperator(address account) external view returns (bool); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/internal/roles/IRoles.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @notice Interface for a contract which implements admin roles. 7 | * @author batu-inal & HardlyDifficult 8 | */ 9 | interface IRoles { 10 | function isAdmin(address account) external view returns (bool); 11 | 12 | function isOperator(address account) external view returns (bool); 13 | } 14 | -------------------------------------------------------------------------------- /contracts/interfaces/standards/royalties/IGetFees.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @notice An interface for communicating fees to 3rd party marketplaces. 7 | * @dev Originally implemented in mainnet contract 0x44d6e8933f8271abcf253c72f9ed7e0e4c0323b3 8 | */ 9 | interface IGetFees { 10 | /** 11 | * @notice Get the recipient addresses to which creator royalties should be sent. 12 | * @dev The expected royalty amounts are communicated with `getFeeBps`. 13 | * @param tokenId The ID of the NFT to get royalties for. 14 | * @return recipients An array of addresses to which royalties should be sent. 15 | */ 16 | function getFeeRecipients(uint256 tokenId) external view returns (address payable[] memory recipients); 17 | 18 | /** 19 | * @notice Get the creator royalty amounts to be sent to each recipient, in basis points. 20 | * @dev The expected recipients are communicated with `getFeeRecipients`. 21 | * @param tokenId The ID of the NFT to get royalties for. 22 | * @return royaltiesInBasisPoints The array of fees to be sent to each recipient, in basis points. 23 | */ 24 | function getFeeBps(uint256 tokenId) external view returns (uint256[] memory royaltiesInBasisPoints); 25 | } 26 | -------------------------------------------------------------------------------- /contracts/interfaces/standards/royalties/IGetRoyalties.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | interface IGetRoyalties { 6 | /** 7 | * @notice Get the creator royalties to be sent. 8 | * @dev The data is the same as when calling `getFeeRecipients` and `getFeeBps` separately. 9 | * @param tokenId The ID of the NFT to get royalties for. 10 | * @return recipients An array of addresses to which royalties should be sent. 11 | * @return royaltiesInBasisPoints The array of fees to be sent to each recipient, in basis points. 12 | */ 13 | function getRoyalties(uint256 tokenId) 14 | external 15 | view 16 | returns (address payable[] memory recipients, uint256[] memory royaltiesInBasisPoints); 17 | } 18 | -------------------------------------------------------------------------------- /contracts/interfaces/standards/royalties/IOwnable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | interface IOwnable { 6 | /** 7 | * @dev Returns the address of the current owner. 8 | */ 9 | function owner() external view returns (address); 10 | } 11 | -------------------------------------------------------------------------------- /contracts/interfaces/standards/royalties/IRoyaltyInfo.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @notice Interface for EIP-2981: NFT Royalty Standard. 7 | * For more see: https://eips.ethereum.org/EIPS/eip-2981. 8 | */ 9 | interface IRoyaltyInfo { 10 | /** 11 | * @notice Get the creator royalties to be sent. 12 | * @param tokenId The ID of the NFT to get royalties for. 13 | * @param salePrice The total price of the sale. 14 | * @return receiver The address to which royalties should be sent. 15 | * @return royaltyAmount The total amount that should be sent to the `receiver`. 16 | */ 17 | function royaltyInfo(uint256 tokenId, uint256 salePrice) 18 | external 19 | view 20 | returns (address receiver, uint256 royaltyAmount); 21 | } 22 | -------------------------------------------------------------------------------- /contracts/interfaces/standards/royalties/ITokenCreator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | interface ITokenCreator { 6 | /** 7 | * @notice Returns the creator of this NFT collection. 8 | * @param tokenId The ID of the NFT to get the creator payment address for. 9 | * @return creator The creator of this collection. 10 | */ 11 | function tokenCreator(uint256 tokenId) external view returns (address payable creator); 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interfaces/subgraph/IMarketDeprecatedAPIs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @dev Historical (currently deprecated) APIs 7 | * that the subgraph depends on for historical transactions. 8 | */ 9 | interface IMarketDeprecatedAPIs { 10 | function getIsPrimary(address nftContract, uint256 tokenId) external view returns (bool isPrimary); 11 | 12 | function getFees( 13 | address nftContract, 14 | uint256 tokenId, 15 | uint256 price 16 | ) 17 | external 18 | view 19 | returns ( 20 | uint256 totalFees, 21 | uint256 creatorRev, 22 | uint256 sellerRev 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /contracts/libraries/AddressLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; 6 | 7 | struct CallWithoutValue { 8 | address target; 9 | bytes callData; 10 | } 11 | 12 | /** 13 | * @title A library for address helpers not already covered by the OZ library. 14 | * @author batu-inal & HardlyDifficult 15 | */ 16 | library AddressLibrary { 17 | using AddressUpgradeable for address; 18 | using AddressUpgradeable for address payable; 19 | 20 | /** 21 | * @notice Calls an external contract with arbitrary data and parse the return value into an address. 22 | * @param externalContract The address of the contract to call. 23 | * @param callData The data to send to the contract. 24 | * @return contractAddress The address of the contract returned by the call. 25 | */ 26 | function callAndReturnContractAddress(address externalContract, bytes calldata callData) 27 | internal 28 | returns (address payable contractAddress) 29 | { 30 | bytes memory returnData = externalContract.functionCall(callData); 31 | contractAddress = abi.decode(returnData, (address)); 32 | require(contractAddress.isContract(), "InternalProxyCall: did not return a contract"); 33 | } 34 | 35 | function callAndReturnContractAddress(CallWithoutValue calldata call) 36 | internal 37 | returns (address payable contractAddress) 38 | { 39 | contractAddress = callAndReturnContractAddress(call.target, call.callData); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/libraries/ArrayLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @title Helper functions for arrays. 7 | * @author batu-inal & HardlyDifficult 8 | */ 9 | library ArrayLibrary { 10 | /** 11 | * @notice Reduces the size of an array if it's greater than the specified max size, 12 | * using the first maxSize elements. 13 | */ 14 | function capLength(address payable[] memory data, uint256 maxLength) internal pure { 15 | if (data.length > maxLength) { 16 | assembly { 17 | mstore(data, maxLength) 18 | } 19 | } 20 | } 21 | 22 | /** 23 | * @notice Reduces the size of an array if it's greater than the specified max size, 24 | * using the first maxSize elements. 25 | */ 26 | function capLength(uint256[] memory data, uint256 maxLength) internal pure { 27 | if (data.length > maxLength) { 28 | assembly { 29 | mstore(data, maxLength) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/libraries/BytesLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | error BytesLibrary_Expected_Address_Not_Found(); 6 | error BytesLibrary_Start_Location_Too_Large(); 7 | 8 | /** 9 | * @title A library for manipulation of byte arrays. 10 | * @author batu-inal & HardlyDifficult 11 | */ 12 | library BytesLibrary { 13 | /// @notice An address is 20 bytes long 14 | uint256 private constant ADDRESS_BYTES_LENGTH = 20; 15 | 16 | /// @notice A signature is 4 bytes long 17 | uint256 private constant SIGNATURE_BYTES_LENGTH = 4; 18 | 19 | /** 20 | * @dev Replace the address at the given location in a byte array if the contents at that location 21 | * match the expected address. 22 | */ 23 | function replaceAtIf( 24 | bytes memory data, 25 | uint256 startLocation, 26 | address expectedAddress, 27 | address newAddress 28 | ) internal pure { 29 | unchecked { 30 | if (startLocation > type(uint256).max - ADDRESS_BYTES_LENGTH) { 31 | revert BytesLibrary_Start_Location_Too_Large(); 32 | } 33 | bytes memory expectedData = abi.encodePacked(expectedAddress); 34 | bytes memory newData = abi.encodePacked(newAddress); 35 | uint256 dataLocation; 36 | for (uint256 i = 0; i < ADDRESS_BYTES_LENGTH; ++i) { 37 | dataLocation = startLocation + i; 38 | if (data[dataLocation] != expectedData[i]) { 39 | revert BytesLibrary_Expected_Address_Not_Found(); 40 | } 41 | data[dataLocation] = newData[i]; 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * @dev Checks if the call data starts with the given function signature. 48 | */ 49 | function startsWith(bytes memory callData, bytes4 functionSig) internal pure returns (bool) { 50 | // A signature is 4 bytes long 51 | if (callData.length < SIGNATURE_BYTES_LENGTH) { 52 | return false; 53 | } 54 | return bytes4(callData) == functionSig; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /contracts/libraries/LockedBalance.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @title Library that handles locked balances efficiently using bit packing. 7 | * @author batu-inal & HardlyDifficult 8 | */ 9 | library LockedBalance { 10 | /// @dev Tracks an account's total lockup per expiration time. 11 | struct Lockup { 12 | uint32 expiration; 13 | uint96 totalAmount; 14 | } 15 | 16 | struct Lockups { 17 | /// @dev Mapping from key to lockups. 18 | /// i) A key represents 2 lockups. The key for a lockup is `index / 2`. 19 | /// For instance, elements with index 25 and 24 would map to the same key. 20 | /// ii) The `value` for the `key` is split into two 128bits which are used to store the metadata for a lockup. 21 | mapping(uint256 => uint256) lockups; 22 | } 23 | 24 | // Masks used to split a uint256 into two equal pieces which represent two individual Lockups. 25 | uint256 private constant last128BitsMask = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; 26 | uint256 private constant first128BitsMask = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000; 27 | 28 | // Masks used to retrieve or set the totalAmount value of a single Lockup. 29 | uint256 private constant firstAmountBitsMask = 0xFFFFFFFF000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; 30 | uint256 private constant secondAmountBitsMask = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000; 31 | 32 | /** 33 | * @notice Clears the lockup at the index. 34 | */ 35 | function del(Lockups storage lockups, uint256 index) internal { 36 | unchecked { 37 | if (index % 2 == 0) { 38 | index /= 2; 39 | lockups.lockups[index] = (lockups.lockups[index] & last128BitsMask); 40 | } else { 41 | index /= 2; 42 | lockups.lockups[index] = (lockups.lockups[index] & first128BitsMask); 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * @notice Sets the Lockup at the provided index. 49 | */ 50 | function set( 51 | Lockups storage lockups, 52 | uint256 index, 53 | uint256 expiration, 54 | uint256 totalAmount 55 | ) internal { 56 | unchecked { 57 | uint256 lockedBalanceBits = totalAmount | (expiration << 96); 58 | if (index % 2 == 0) { 59 | // set first 128 bits. 60 | index /= 2; 61 | lockups.lockups[index] = (lockups.lockups[index] & last128BitsMask) | (lockedBalanceBits << 128); 62 | } else { 63 | // set last 128 bits. 64 | index /= 2; 65 | lockups.lockups[index] = (lockups.lockups[index] & first128BitsMask) | lockedBalanceBits; 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * @notice Sets only the totalAmount for a lockup at the index. 72 | */ 73 | function setTotalAmount( 74 | Lockups storage lockups, 75 | uint256 index, 76 | uint256 totalAmount 77 | ) internal { 78 | unchecked { 79 | if (index % 2 == 0) { 80 | index /= 2; 81 | lockups.lockups[index] = (lockups.lockups[index] & firstAmountBitsMask) | (totalAmount << 128); 82 | } else { 83 | index /= 2; 84 | lockups.lockups[index] = (lockups.lockups[index] & secondAmountBitsMask) | totalAmount; 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * @notice Returns the Lockup at the provided index. 91 | * @dev To get the lockup stored in the *first* 128 bits (first slot/lockup): 92 | * - we remove the last 128 bits (done by >> 128) 93 | * To get the lockup stored in the *last* 128 bits (second slot/lockup): 94 | * - we take the last 128 bits (done by % (2**128)) 95 | * Once the lockup is obtained: 96 | * - get `expiration` by peaking at the first 32 bits (done by >> 96) 97 | * - get `totalAmount` by peaking at the last 96 bits (done by % (2**96)) 98 | */ 99 | function get(Lockups storage lockups, uint256 index) internal view returns (Lockup memory balance) { 100 | unchecked { 101 | uint256 lockupMetadata = lockups.lockups[index / 2]; 102 | if (lockupMetadata == 0) { 103 | return balance; 104 | } 105 | uint128 lockedBalanceBits; 106 | if (index % 2 == 0) { 107 | // use first 128 bits. 108 | lockedBalanceBits = uint128(lockupMetadata >> 128); 109 | } else { 110 | // use last 128 bits. 111 | lockedBalanceBits = uint128(lockupMetadata % (2**128)); 112 | } 113 | // unpack the bits to retrieve the Lockup. 114 | balance.expiration = uint32(lockedBalanceBits >> 96); 115 | balance.totalAmount = uint96(lockedBalanceBits % (2**96)); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /contracts/libraries/OZERC165Checker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * From https://github.com/OpenZeppelin/openzeppelin-contracts 7 | * Copying the method below which is currently unreleased. 8 | */ 9 | 10 | import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 11 | 12 | /** 13 | * @title Library to query ERC165 support. 14 | * @dev Library used to query support of an interface declared via {IERC165}. 15 | * 16 | * Note that these functions return the actual result of the query: they do not 17 | * `revert` if an interface is not supported. It is up to the caller to decide 18 | * what to do in these cases. 19 | */ 20 | library OZERC165Checker { 21 | /** 22 | * @notice Query if a contract implements an interface, does not check ERC165 support 23 | * @param account The address of the contract to query for support of an interface 24 | * @param interfaceId The interface identifier, as specified in ERC-165 25 | * @return true if the contract at account indicates support of the interface with 26 | * identifier interfaceId, false otherwise 27 | * @dev Assumes that account contains a contract that supports ERC165, otherwise 28 | * the behavior of this method is undefined. This precondition can be checked 29 | * with {supportsERC165}. 30 | * Interface identification is specified in ERC-165. 31 | */ 32 | function supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId) internal view returns (bool) { 33 | bytes memory encodedParams = abi.encodeWithSelector(IERC165.supportsInterface.selector, interfaceId); 34 | (bool success, bytes memory result) = account.staticcall{ gas: 30_000 }(encodedParams); 35 | if (result.length < 32) return false; 36 | return success && abi.decode(result, (uint256)) > 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /contracts/mixins/collections/CollectionRoyalties.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; 6 | 7 | import "../../interfaces/standards/royalties/IGetFees.sol"; 8 | import "../../interfaces/standards/royalties/IGetRoyalties.sol"; 9 | import "../../interfaces/standards/royalties/IRoyaltyInfo.sol"; 10 | import "../../interfaces/standards/royalties/ITokenCreator.sol"; 11 | 12 | import "../shared/Constants.sol"; 13 | 14 | /** 15 | * @title Defines various royalty APIs for broad marketplace support. 16 | * @author batu-inal & HardlyDifficult 17 | */ 18 | abstract contract CollectionRoyalties is IGetRoyalties, IGetFees, IRoyaltyInfo, ITokenCreator, ERC165Upgradeable { 19 | /** 20 | * @inheritdoc IGetFees 21 | */ 22 | function getFeeRecipients(uint256 tokenId) external view returns (address payable[] memory recipients) { 23 | recipients = new address payable[](1); 24 | recipients[0] = getTokenCreatorPaymentAddress(tokenId); 25 | } 26 | 27 | /** 28 | * @inheritdoc IGetFees 29 | * @dev The tokenId param is ignored since all NFTs return the same value. 30 | */ 31 | function getFeeBps( 32 | uint256 /* tokenId */ 33 | ) external pure returns (uint256[] memory royaltiesInBasisPoints) { 34 | royaltiesInBasisPoints = new uint256[](1); 35 | royaltiesInBasisPoints[0] = ROYALTY_IN_BASIS_POINTS; 36 | } 37 | 38 | /** 39 | * @inheritdoc IGetRoyalties 40 | */ 41 | function getRoyalties(uint256 tokenId) 42 | external 43 | view 44 | returns (address payable[] memory recipients, uint256[] memory royaltiesInBasisPoints) 45 | { 46 | recipients = new address payable[](1); 47 | recipients[0] = getTokenCreatorPaymentAddress(tokenId); 48 | royaltiesInBasisPoints = new uint256[](1); 49 | royaltiesInBasisPoints[0] = ROYALTY_IN_BASIS_POINTS; 50 | } 51 | 52 | /** 53 | * @notice The address to pay the creator proceeds/royalties for the collection. 54 | * @param tokenId The ID of the NFT to get the creator payment address for. 55 | * @return creatorPaymentAddress The address to which royalties should be paid. 56 | */ 57 | function getTokenCreatorPaymentAddress(uint256 tokenId) 58 | public 59 | view 60 | virtual 61 | returns (address payable creatorPaymentAddress); 62 | 63 | /** 64 | * @inheritdoc IRoyaltyInfo 65 | */ 66 | function royaltyInfo(uint256 tokenId, uint256 salePrice) 67 | external 68 | view 69 | returns (address receiver, uint256 royaltyAmount) 70 | { 71 | receiver = getTokenCreatorPaymentAddress(tokenId); 72 | unchecked { 73 | royaltyAmount = salePrice / ROYALTY_RATIO; 74 | } 75 | } 76 | 77 | /** 78 | * @inheritdoc IERC165Upgradeable 79 | * @dev Checks the supported royalty interfaces. 80 | */ 81 | function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool interfaceSupported) { 82 | interfaceSupported = (interfaceId == type(IRoyaltyInfo).interfaceId || 83 | interfaceId == type(ITokenCreator).interfaceId || 84 | interfaceId == type(IGetRoyalties).interfaceId || 85 | interfaceId == type(IGetFees).interfaceId || 86 | super.supportsInterface(interfaceId)); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /contracts/mixins/collections/SequentialMintCollection.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol"; 7 | 8 | import "../../interfaces/standards/royalties/ITokenCreator.sol"; 9 | 10 | /** 11 | * @title Extends the OZ ERC721 implementation for collections which mint sequential token IDs. 12 | * @author batu-inal & HardlyDifficult 13 | */ 14 | abstract contract SequentialMintCollection is ITokenCreator, Initializable, ERC721BurnableUpgradeable { 15 | /****** Slot 0 (after inheritance) ******/ 16 | /** 17 | * @notice The creator/owner of this NFT collection. 18 | * @dev This is the default royalty recipient if a different `paymentAddress` was not provided. 19 | * @return The collection's creator/owner address. 20 | */ 21 | address payable public owner; 22 | 23 | /** 24 | * @notice The tokenId of the most recently created NFT. 25 | * @dev Minting starts at tokenId 1. Each mint will use this value + 1. 26 | * @return The most recently minted tokenId, or 0 if no NFTs have been minted yet. 27 | */ 28 | uint32 public latestTokenId; 29 | 30 | /** 31 | * @notice The max tokenId which can be minted. 32 | * @dev This max may be less than the final `totalSupply` if 1 or more tokens were burned. 33 | * @return The max tokenId which can be minted. 34 | */ 35 | uint32 public maxTokenId; 36 | 37 | /** 38 | * @notice Tracks how many tokens have been burned. 39 | * @dev This number is used to calculate the total supply efficiently. 40 | */ 41 | uint32 private burnCounter; 42 | 43 | /****** End of storage ******/ 44 | 45 | /** 46 | * @notice Emitted when the max tokenId supported by this collection is updated. 47 | * @param maxTokenId The new max tokenId. All NFTs in this collection will have a tokenId less than 48 | * or equal to this value. 49 | */ 50 | event MaxTokenIdUpdated(uint256 indexed maxTokenId); 51 | 52 | /** 53 | * @notice Emitted when this collection is self destructed by the creator/owner/admin. 54 | * @param admin The account which requested this contract be self destructed. 55 | */ 56 | event SelfDestruct(address indexed admin); 57 | 58 | modifier onlyOwner() { 59 | require(msg.sender == owner, "SequentialMintCollection: Caller is not owner"); 60 | _; 61 | } 62 | 63 | function _initializeSequentialMintCollection(address payable _creator, uint32 _maxTokenId) internal onlyInitializing { 64 | owner = _creator; 65 | maxTokenId = _maxTokenId; 66 | } 67 | 68 | /** 69 | * @notice Allows the collection owner to destroy this contract only if 70 | * no NFTs have been minted yet or the minted NFTs have been burned. 71 | */ 72 | function _selfDestruct() internal { 73 | require(totalSupply() == 0, "SequentialMintCollection: Any NFTs minted must be burned first"); 74 | 75 | emit SelfDestruct(msg.sender); 76 | selfdestruct(payable(msg.sender)); 77 | } 78 | 79 | /** 80 | * @notice Allows the owner to set a max tokenID. 81 | * This provides a guarantee to collectors about the limit of this collection contract, if applicable. 82 | * @dev Once this value has been set, it may be decreased but can never be increased. 83 | * @param _maxTokenId The max tokenId to set, all NFTs must have a tokenId less than or equal to this value. 84 | */ 85 | function _updateMaxTokenId(uint32 _maxTokenId) internal { 86 | require(_maxTokenId != 0, "SequentialMintCollection: Max token ID may not be cleared"); 87 | require(maxTokenId == 0 || _maxTokenId < maxTokenId, "SequentialMintCollection: Max token ID may not increase"); 88 | require(latestTokenId <= _maxTokenId, "SequentialMintCollection: Max token ID must be >= last mint"); 89 | 90 | maxTokenId = _maxTokenId; 91 | emit MaxTokenIdUpdated(_maxTokenId); 92 | } 93 | 94 | function _burn(uint256 tokenId) internal virtual override { 95 | unchecked { 96 | // Number of burned tokens cannot exceed latestTokenId which is the same size. 97 | ++burnCounter; 98 | } 99 | super._burn(tokenId); 100 | } 101 | 102 | /** 103 | * @inheritdoc ITokenCreator 104 | * @dev The tokenId param is ignored since all NFTs return the same value. 105 | */ 106 | function tokenCreator( 107 | uint256 /* tokenId */ 108 | ) external view returns (address payable creator) { 109 | creator = owner; 110 | } 111 | 112 | /** 113 | * @notice Returns the total amount of tokens stored by the contract. 114 | * @dev From the ERC-721 enumerable standard. 115 | * @return supply The total number of NFTs tracked by this contract. 116 | */ 117 | function totalSupply() public view returns (uint256 supply) { 118 | unchecked { 119 | // Number of tokens minted is always >= burned tokens. 120 | supply = latestTokenId - burnCounter; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /contracts/mixins/nftDropMarket/NFTDropMarketCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @title A place for common modifiers and functions used by various market mixins, if any. 7 | * @dev This also leaves a gap which can be used to add a new mixin to the top of the inheritance tree. 8 | * @author batu-inal & HardlyDifficult 9 | */ 10 | abstract contract NFTDropMarketCore { 11 | /** 12 | * @notice This empty reserved space is put in place to allow future versions to add new 13 | * variables without shifting down storage in the inheritance chain. 14 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 15 | */ 16 | uint256[1_000] private __gap; 17 | } 18 | -------------------------------------------------------------------------------- /contracts/mixins/nftDropMarket/NFTDropMarketFixedPriceSale.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; 6 | import "@openzeppelin/contracts/access/IAccessControl.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 8 | import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; 9 | 10 | import "../../interfaces/internal/INFTDropCollectionMint.sol"; 11 | 12 | import "../../libraries/OZERC165Checker.sol"; 13 | import "../shared/Constants.sol"; 14 | import "../shared/MarketFees.sol"; 15 | 16 | /// @param limitPerAccount The limit of tokens an account can purchase. 17 | error NFTDropMarketFixedPriceSale_Cannot_Buy_More_Than_Limit(uint256 limitPerAccount); 18 | error NFTDropMarketFixedPriceSale_Limit_Per_Account_Must_Be_Set(); 19 | error NFTDropMarketFixedPriceSale_Mint_Permission_Required(); 20 | error NFTDropMarketFixedPriceSale_Must_Buy_At_Least_One_Token(); 21 | error NFTDropMarketFixedPriceSale_Must_Have_Sale_In_Progress(); 22 | error NFTDropMarketFixedPriceSale_Must_Not_Be_Sold_Out(); 23 | error NFTDropMarketFixedPriceSale_Must_Not_Have_Pending_Sale(); 24 | error NFTDropMarketFixedPriceSale_Must_Support_Collection_Mint_Interface(); 25 | error NFTDropMarketFixedPriceSale_Must_Support_ERC721(); 26 | error NFTDropMarketFixedPriceSale_Only_Callable_By_Collection_Owner(); 27 | /// @param mintCost The total cost for this purchase. 28 | error NFTDropMarketFixedPriceSale_Too_Much_Value_Provided(uint256 mintCost); 29 | error NFTDropMarketFixedPriceSale_Mint_Count_Mismatch(uint256 targetBalance); 30 | 31 | /** 32 | * @title Allows creators to list a drop collection for sale at a fixed price point. 33 | * @dev Listing a collection for sale in this market requires the collection to implement 34 | * the functions in `INFTDropCollectionMint` and to register that interface with ERC165. 35 | * Additionally the collection must implement access control, or more specifically: 36 | * `hasRole(bytes32(0), msg.sender)` must return true when called from the creator or admin's account 37 | * and `hasRole(keccak256("MINTER_ROLE", address(this)))` must return true for this market's address. 38 | * @author batu-inal & HardlyDifficult 39 | */ 40 | abstract contract NFTDropMarketFixedPriceSale is MarketFees { 41 | using AddressUpgradeable for address; 42 | using AddressUpgradeable for address payable; 43 | using ERC165Checker for address; 44 | using OZERC165Checker for address; 45 | 46 | /** 47 | * @notice Configuration for the terms of the sale. 48 | * @dev This structure is packed in order to consume just a single slot. 49 | */ 50 | struct FixedPriceSaleConfig { 51 | /** 52 | * @notice The seller for the drop. 53 | */ 54 | address payable seller; 55 | /** 56 | * @notice The fixed price per NFT in the collection. 57 | * @dev The maximum price that can be set on an NFT is ~1.2M (2^80/10^18) ETH. 58 | */ 59 | uint80 price; 60 | /** 61 | * @notice The max number of NFTs an account may have while minting. 62 | */ 63 | uint16 limitPerAccount; 64 | } 65 | 66 | /** 67 | * @notice Stores the current sale information for all drop contracts. 68 | */ 69 | mapping(address => FixedPriceSaleConfig) private nftContractToFixedPriceSaleConfig; 70 | 71 | /** 72 | * @notice The `role` type used to validate drop collections have granted this market access to mint. 73 | * @return `keccak256("MINTER_ROLE")` 74 | */ 75 | bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); 76 | 77 | /** 78 | * @notice Emitted when a collection is listed for sale. 79 | * @param nftContract The address of the NFT drop collection. 80 | * @param seller The address for the seller which listed this for sale. 81 | * @param price The price per NFT minted. 82 | * @param limitPerAccount The max number of NFTs an account may have while minting. 83 | */ 84 | event CreateFixedPriceSale( 85 | address indexed nftContract, 86 | address indexed seller, 87 | uint256 price, 88 | uint256 limitPerAccount 89 | ); 90 | 91 | /** 92 | * @notice Emitted when NFTs are minted from the drop. 93 | * @dev The total price paid by the buyer is `totalFees + creatorRev`. 94 | * @param nftContract The address of the NFT drop collection. 95 | * @param buyer The address of the buyer. 96 | * @param firstTokenId The tokenId for the first NFT minted. 97 | * The other minted tokens are assigned sequentially, so `firstTokenId` - `firstTokenId + count - 1` were minted. 98 | * @param count The number of NFTs minted. 99 | * @param totalFees The amount of ETH that was sent to Foundation & referrals for this sale. 100 | * @param creatorRev The amount of ETH that was sent to the creator for this sale. 101 | */ 102 | event MintFromFixedPriceDrop( 103 | address indexed nftContract, 104 | address indexed buyer, 105 | uint256 indexed firstTokenId, 106 | uint256 count, 107 | uint256 totalFees, 108 | uint256 creatorRev 109 | ); 110 | 111 | /** 112 | * @notice Create a fixed price sale drop. 113 | * @param nftContract The address of the NFT drop collection. 114 | * @param price The price per NFT minted. 115 | * Set price to 0 for a first come first serve airdrop-like drop. 116 | * @param limitPerAccount The max number of NFTs an account may have while minting. 117 | * @dev Notes: 118 | * a) The sale is final and can not be updated or canceled. 119 | * b) The sale is immediately kicked off. 120 | * c) Any collection that abides by `INFTDropCollectionMint` and `IAccessControl` is supported. 121 | */ 122 | /* solhint-disable-next-line code-complexity */ 123 | function createFixedPriceSale( 124 | address nftContract, 125 | uint80 price, 126 | uint16 limitPerAccount 127 | ) external { 128 | // Confirm the drop collection is supported 129 | if (!nftContract.supportsInterface(type(INFTDropCollectionMint).interfaceId)) { 130 | revert NFTDropMarketFixedPriceSale_Must_Support_Collection_Mint_Interface(); 131 | } 132 | // The check above already confirmed general 165 support 133 | if (!nftContract.supportsERC165InterfaceUnchecked(type(IERC721).interfaceId)) { 134 | revert NFTDropMarketFixedPriceSale_Must_Support_ERC721(); 135 | } 136 | if (INFTDropCollectionMint(nftContract).numberOfTokensAvailableToMint() == 0) { 137 | revert NFTDropMarketFixedPriceSale_Must_Not_Be_Sold_Out(); 138 | } 139 | 140 | // Use the AccessControl interface to confirm the msg.sender has permissions to list. 141 | if (!IAccessControl(nftContract).hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { 142 | revert NFTDropMarketFixedPriceSale_Only_Callable_By_Collection_Owner(); 143 | } 144 | // And that this contract has permission to mint. 145 | if (!IAccessControl(nftContract).hasRole(MINTER_ROLE, address(this))) { 146 | revert NFTDropMarketFixedPriceSale_Mint_Permission_Required(); 147 | } 148 | 149 | // Validate input params. 150 | if (limitPerAccount == 0) { 151 | revert NFTDropMarketFixedPriceSale_Limit_Per_Account_Must_Be_Set(); 152 | } 153 | // Any price is supported, including 0. 154 | 155 | // Confirm this collection has not already been listed. 156 | FixedPriceSaleConfig storage saleConfig = nftContractToFixedPriceSaleConfig[nftContract]; 157 | if (saleConfig.seller != payable(0)) { 158 | revert NFTDropMarketFixedPriceSale_Must_Not_Have_Pending_Sale(); 159 | } 160 | 161 | // Save the sale details. 162 | saleConfig.seller = payable(msg.sender); 163 | saleConfig.price = price; 164 | saleConfig.limitPerAccount = limitPerAccount; 165 | emit CreateFixedPriceSale(nftContract, msg.sender, price, limitPerAccount); 166 | } 167 | 168 | /** 169 | * @notice Used to mint `count` number of NFTs from the collection. 170 | * @param nftContract The address of the NFT drop collection. 171 | * @param count The number of NFTs to mint. 172 | * @param buyReferrer The address which referred this purchase, or address(0) if n/a. 173 | * @return firstTokenId The tokenId for the first NFT minted. 174 | * The other minted tokens are assigned sequentially, so `firstTokenId` - `firstTokenId + count - 1` were minted. 175 | * @dev This call may revert if the collection has sold out, has an insufficient number of tokens available, 176 | * or if the market's minter permissions were removed. 177 | * If insufficient msg.value is included, the msg.sender's available FETH token balance will be used. 178 | */ 179 | function mintFromFixedPriceSale( 180 | address nftContract, 181 | uint16 count, 182 | address payable buyReferrer 183 | ) external payable returns (uint256 firstTokenId) { 184 | // Validate input params. 185 | if (count == 0) { 186 | revert NFTDropMarketFixedPriceSale_Must_Buy_At_Least_One_Token(); 187 | } 188 | 189 | FixedPriceSaleConfig memory saleConfig = nftContractToFixedPriceSaleConfig[nftContract]; 190 | 191 | // Confirm that the buyer will not exceed the limit specified after minting. 192 | uint256 targetBalance = IERC721(nftContract).balanceOf(msg.sender) + count; 193 | if (targetBalance > saleConfig.limitPerAccount) { 194 | if (saleConfig.limitPerAccount == 0) { 195 | // Provide a more targeted error if the collection has not been listed. 196 | revert NFTDropMarketFixedPriceSale_Must_Have_Sale_In_Progress(); 197 | } 198 | revert NFTDropMarketFixedPriceSale_Cannot_Buy_More_Than_Limit(saleConfig.limitPerAccount); 199 | } 200 | 201 | // Calculate the total cost, considering the `count` requested. 202 | uint256 mintCost; 203 | unchecked { 204 | // Can not overflow as 2^80 * 2^16 == 2^96 max which fits in 256 bits. 205 | mintCost = uint256(saleConfig.price) * count; 206 | } 207 | 208 | // The sale price is immutable so the buyer is aware of how much they will be paying when their tx is broadcasted. 209 | if (msg.value > mintCost) { 210 | // Since price is known ahead of time, if too much ETH is sent then something went wrong. 211 | revert NFTDropMarketFixedPriceSale_Too_Much_Value_Provided(mintCost); 212 | } 213 | // Withdraw from the user's available FETH balance if insufficient msg.value was included. 214 | _tryUseFETHBalance(mintCost, false); 215 | 216 | // Mint the NFTs. 217 | firstTokenId = INFTDropCollectionMint(nftContract).mintCountTo(count, msg.sender); 218 | 219 | if (IERC721(nftContract).balanceOf(msg.sender) != targetBalance) { 220 | revert NFTDropMarketFixedPriceSale_Mint_Count_Mismatch(targetBalance); 221 | } 222 | 223 | // Distribute revenue from this sale. 224 | (uint256 totalFees, uint256 creatorRev, ) = _distributeFunds( 225 | nftContract, 226 | firstTokenId, 227 | saleConfig.seller, 228 | mintCost, 229 | buyReferrer 230 | ); 231 | 232 | emit MintFromFixedPriceDrop(nftContract, msg.sender, firstTokenId, count, totalFees, creatorRev); 233 | } 234 | 235 | /** 236 | * @notice Returns the max number of NFTs a given account may mint. 237 | * @param nftContract The address of the NFT drop collection. 238 | * @param user The address of the user which will be minting. 239 | * @return numberThatCanBeMinted How many NFTs the user can mint. 240 | */ 241 | function getAvailableCountFromFixedPriceSale(address nftContract, address user) 242 | external 243 | view 244 | returns (uint256 numberThatCanBeMinted) 245 | { 246 | (, , uint256 limitPerAccount, uint256 numberOfTokensAvailableToMint, bool marketCanMint) = getFixedPriceSale( 247 | nftContract 248 | ); 249 | if (!marketCanMint) { 250 | // No one can mint in the current state. 251 | return 0; 252 | } 253 | uint256 currentBalance = IERC721(nftContract).balanceOf(user); 254 | if (currentBalance >= limitPerAccount) { 255 | // User has exhausted their limit. 256 | return 0; 257 | } 258 | 259 | unchecked { 260 | numberThatCanBeMinted = limitPerAccount - currentBalance; 261 | } 262 | if (numberThatCanBeMinted > numberOfTokensAvailableToMint) { 263 | // User has more tokens available than the collection has available. 264 | numberThatCanBeMinted = numberOfTokensAvailableToMint; 265 | } 266 | } 267 | 268 | /** 269 | * @notice Returns details for a drop collection's fixed price sale. 270 | * @param nftContract The address of the NFT drop collection. 271 | * @return seller The address of the seller which listed this drop for sale. 272 | * This value will be address(0) if the collection is not listed or has sold out. 273 | * @return price The price per NFT minted. 274 | * @return limitPerAccount The max number of NFTs an account may have while minting. 275 | * @return numberOfTokensAvailableToMint The total number of NFTs that may still be minted. 276 | * @return marketCanMint True if this contract has permissions to mint from the given collection. 277 | */ 278 | function getFixedPriceSale(address nftContract) 279 | public 280 | view 281 | returns ( 282 | address payable seller, 283 | uint256 price, 284 | uint256 limitPerAccount, 285 | uint256 numberOfTokensAvailableToMint, 286 | bool marketCanMint 287 | ) 288 | { 289 | try INFTDropCollectionMint(nftContract).numberOfTokensAvailableToMint() returns (uint256 count) { 290 | if (count != 0) { 291 | try IAccessControl(nftContract).hasRole(MINTER_ROLE, address(this)) returns (bool hasRole) { 292 | marketCanMint = hasRole; 293 | } catch { 294 | // The contract is not supported - return default values. 295 | return (payable(0), 0, 0, 0, false); 296 | } 297 | 298 | FixedPriceSaleConfig memory saleConfig = nftContractToFixedPriceSaleConfig[nftContract]; 299 | seller = saleConfig.seller; 300 | price = saleConfig.price; 301 | limitPerAccount = saleConfig.limitPerAccount; 302 | numberOfTokensAvailableToMint = count; 303 | } 304 | // Else minted completed -- return default values. 305 | } catch // solhint-disable-next-line no-empty-blocks 306 | { 307 | // Contract not supported or self destructed - return default values 308 | } 309 | } 310 | 311 | /** 312 | * @inheritdoc MarketSharedCore 313 | * @dev Returns the seller for a collection if listed and not already sold out. 314 | */ 315 | function _getSellerOf( 316 | address nftContract, 317 | uint256 /* tokenId */ 318 | ) internal view virtual override returns (address payable seller) { 319 | (seller, , , , ) = getFixedPriceSale(nftContract); 320 | } 321 | 322 | /** 323 | * @notice This empty reserved space is put in place to allow future versions to add new 324 | * variables without shifting down storage in the inheritance chain. 325 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 326 | */ 327 | uint256[1_000] private __gap; 328 | } 329 | -------------------------------------------------------------------------------- /contracts/mixins/nftMarket/NFTMarketAuction.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 6 | 7 | /** 8 | * @title An abstraction layer for auctions. 9 | * @dev This contract can be expanded with reusable calls and data as more auction types are added. 10 | * @author batu-inal & HardlyDifficult 11 | */ 12 | abstract contract NFTMarketAuction is Initializable { 13 | /** 14 | * @notice A global id for auctions of any type. 15 | */ 16 | uint256 private nextAuctionId; 17 | 18 | /** 19 | * @notice Called once to configure the contract after the initial proxy deployment. 20 | * @dev This sets the initial auction id to 1, making the first auction cheaper 21 | * and id 0 represents no auction found. 22 | */ 23 | function _initializeNFTMarketAuction() internal onlyInitializing { 24 | nextAuctionId = 1; 25 | } 26 | 27 | /** 28 | * @notice Returns id to assign to the next auction. 29 | */ 30 | function _getNextAndIncrementAuctionId() internal returns (uint256) { 31 | // AuctionId cannot overflow 256 bits. 32 | unchecked { 33 | return nextAuctionId++; 34 | } 35 | } 36 | 37 | /** 38 | * @notice This empty reserved space is put in place to allow future versions to add new 39 | * variables without shifting down storage in the inheritance chain. 40 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 41 | */ 42 | uint256[1_000] private __gap; 43 | } 44 | -------------------------------------------------------------------------------- /contracts/mixins/nftMarket/NFTMarketCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; 7 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 8 | 9 | import "../../interfaces/internal/IFethMarket.sol"; 10 | 11 | import "../shared/Constants.sol"; 12 | import "../shared/MarketSharedCore.sol"; 13 | 14 | error NFTMarketCore_Seller_Not_Found(); 15 | 16 | /** 17 | * @title A place for common modifiers and functions used by various NFTMarket mixins, if any. 18 | * @dev This also leaves a gap which can be used to add a new mixin to the top of the inheritance tree. 19 | * @author batu-inal & HardlyDifficult 20 | */ 21 | abstract contract NFTMarketCore is Initializable, MarketSharedCore { 22 | using AddressUpgradeable for address; 23 | using AddressUpgradeable for address payable; 24 | 25 | /** 26 | * @notice If there is a buy price at this amount or lower, accept that and return true. 27 | */ 28 | function _autoAcceptBuyPrice( 29 | address nftContract, 30 | uint256 tokenId, 31 | uint256 amount 32 | ) internal virtual returns (bool); 33 | 34 | /** 35 | * @notice If there is a valid offer at the given price or higher, accept that and return true. 36 | */ 37 | function _autoAcceptOffer( 38 | address nftContract, 39 | uint256 tokenId, 40 | uint256 minAmount 41 | ) internal virtual returns (bool); 42 | 43 | /** 44 | * @notice Notify implementors when an auction has received its first bid. 45 | * Once a bid is received the sale is guaranteed to the auction winner 46 | * and other sale mechanisms become unavailable. 47 | * @dev Implementors of this interface should update internal state to reflect an auction has been kicked off. 48 | */ 49 | function _beforeAuctionStarted( 50 | address, /*nftContract*/ 51 | uint256 /*tokenId*/ // solhint-disable-next-line no-empty-blocks 52 | ) internal virtual { 53 | // No-op 54 | } 55 | 56 | /** 57 | * @notice Cancel the `msg.sender`'s offer if there is one, freeing up their FETH balance. 58 | * @dev This should be used when it does not make sense to keep the original offer around, 59 | * e.g. if a collector accepts a Buy Price then keeping the offer around is not necessary. 60 | */ 61 | function _cancelSendersOffer(address nftContract, uint256 tokenId) internal virtual; 62 | 63 | /** 64 | * @notice Transfers the NFT from escrow and clears any state tracking this escrowed NFT. 65 | * @param authorizeSeller The address of the seller pending authorization. 66 | * Once it's been authorized by one of the escrow managers, it should be set to address(0) 67 | * indicated that it's no longer pending authorization. 68 | */ 69 | function _transferFromEscrow( 70 | address nftContract, 71 | uint256 tokenId, 72 | address recipient, 73 | address authorizeSeller 74 | ) internal virtual { 75 | if (authorizeSeller != address(0)) { 76 | revert NFTMarketCore_Seller_Not_Found(); 77 | } 78 | IERC721(nftContract).transferFrom(address(this), recipient, tokenId); 79 | } 80 | 81 | /** 82 | * @notice Transfers the NFT from escrow unless there is another reason for it to remain in escrow. 83 | */ 84 | function _transferFromEscrowIfAvailable( 85 | address nftContract, 86 | uint256 tokenId, 87 | address recipient 88 | ) internal virtual { 89 | _transferFromEscrow(nftContract, tokenId, recipient, address(0)); 90 | } 91 | 92 | /** 93 | * @notice Transfers an NFT into escrow, 94 | * if already there this requires the msg.sender is authorized to manage the sale of this NFT. 95 | */ 96 | function _transferToEscrow(address nftContract, uint256 tokenId) internal virtual { 97 | IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId); 98 | } 99 | 100 | /** 101 | * @dev Determines the minimum amount when increasing an existing offer or bid. 102 | */ 103 | function _getMinIncrement(uint256 currentAmount) internal pure returns (uint256) { 104 | uint256 minIncrement = currentAmount; 105 | unchecked { 106 | minIncrement /= MIN_PERCENT_INCREMENT_DENOMINATOR; 107 | } 108 | if (minIncrement == 0) { 109 | // Since minIncrement reduces from the currentAmount, this cannot overflow. 110 | // The next amount must be at least 1 wei greater than the current. 111 | return currentAmount + 1; 112 | } 113 | 114 | return minIncrement + currentAmount; 115 | } 116 | 117 | /** 118 | * @inheritdoc MarketSharedCore 119 | */ 120 | function _getSellerOf(address nftContract, uint256 tokenId) 121 | internal 122 | view 123 | virtual 124 | override 125 | returns (address payable seller) 126 | // solhint-disable-next-line no-empty-blocks 127 | { 128 | // No-op by default 129 | } 130 | 131 | /** 132 | * @inheritdoc MarketSharedCore 133 | */ 134 | function _getSellerOrOwnerOf(address nftContract, uint256 tokenId) 135 | internal 136 | view 137 | override 138 | returns (address payable sellerOrOwner) 139 | { 140 | sellerOrOwner = _getSellerOf(nftContract, tokenId); 141 | if (sellerOrOwner == address(0)) { 142 | sellerOrOwner = payable(IERC721(nftContract).ownerOf(tokenId)); 143 | } 144 | } 145 | 146 | /** 147 | * @notice Checks if an escrowed NFT is currently in active auction. 148 | * @return Returns false if the auction has ended, even if it has not yet been settled. 149 | */ 150 | function _isInActiveAuction(address nftContract, uint256 tokenId) internal view virtual returns (bool); 151 | 152 | /** 153 | * @notice This empty reserved space is put in place to allow future versions to add new 154 | * variables without shifting down storage in the inheritance chain. 155 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 156 | * @dev 50 slots were consumed by adding `ReentrancyGuard`. 157 | */ 158 | uint256[450] private __gap; 159 | } 160 | -------------------------------------------------------------------------------- /contracts/mixins/nftMarket/NFTMarketPrivateSaleGap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @title Reserves space previously occupied by private sales. 7 | * @author batu-inal & HardlyDifficult 8 | */ 9 | abstract contract NFTMarketPrivateSaleGap { 10 | // Original data: 11 | // bytes32 private __gap_was_DOMAIN_SEPARATOR; 12 | // mapping(address => mapping(uint256 => mapping(address => mapping(address => mapping(uint256 => 13 | // mapping(uint256 => bool)))))) private privateSaleInvalidated; 14 | // uint256[999] private __gap; 15 | 16 | /** 17 | * @notice This empty reserved space is put in place to allow future versions to add new 18 | * variables without shifting down storage in the inheritance chain. 19 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 20 | * @dev 1 slot was consumed by privateSaleInvalidated. 21 | */ 22 | uint256[1001] private __gap; 23 | } 24 | -------------------------------------------------------------------------------- /contracts/mixins/roles/AdminRole.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; 7 | 8 | /** 9 | * @title Defines a role for admin accounts. 10 | * @dev Wraps the default admin role from OpenZeppelin's AccessControl for easy integration. 11 | * @author batu-inal & HardlyDifficult 12 | */ 13 | abstract contract AdminRole is Initializable, AccessControlUpgradeable { 14 | modifier onlyAdmin() { 15 | require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "AdminRole: caller does not have the Admin role"); 16 | _; 17 | } 18 | 19 | function _initializeAdminRole(address admin) internal onlyInitializing { 20 | // Grant the role to a specified account 21 | _grantRole(DEFAULT_ADMIN_ROLE, admin); 22 | } 23 | 24 | /** 25 | * @notice Adds an account as an approved admin. 26 | * @dev Only callable by existing admins, as enforced by `grantRole`. 27 | * @param account The address to be approved. 28 | */ 29 | function grantAdmin(address account) external { 30 | grantRole(DEFAULT_ADMIN_ROLE, account); 31 | } 32 | 33 | /** 34 | * @notice Removes an account from the set of approved admins. 35 | * @dev Only callable by existing admins, as enforced by `revokeRole`. 36 | * @param account The address to be removed. 37 | */ 38 | function revokeAdmin(address account) external { 39 | revokeRole(DEFAULT_ADMIN_ROLE, account); 40 | } 41 | 42 | /** 43 | * @notice Checks if the account provided is an admin. 44 | * @param account The address to check. 45 | * @return approved True if the account is an admin. 46 | * @dev This call is used by the royalty registry contract. 47 | */ 48 | function isAdmin(address account) public view returns (bool approved) { 49 | approved = hasRole(DEFAULT_ADMIN_ROLE, account); 50 | } 51 | 52 | /** 53 | * @notice This empty reserved space is put in place to allow future versions to add new 54 | * variables without shifting down storage in the inheritance chain. 55 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 56 | */ 57 | uint256[1_000] private __gap; 58 | } 59 | -------------------------------------------------------------------------------- /contracts/mixins/roles/MinterRole.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 6 | import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; 7 | 8 | import "./AdminRole.sol"; 9 | 10 | /** 11 | * @title Defines a role for minter accounts. 12 | * @dev Wraps a role from OpenZeppelin's AccessControl for easy integration. 13 | * @author batu-inal & HardlyDifficult 14 | */ 15 | abstract contract MinterRole is Initializable, AccessControlUpgradeable, AdminRole { 16 | /** 17 | * @notice The `role` type used for approve minters. 18 | * @return `keccak256("MINTER_ROLE")` 19 | */ 20 | bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); 21 | 22 | modifier onlyMinterOrAdmin() { 23 | require(isMinter(msg.sender) || isAdmin(msg.sender), "MinterRole: Must have the minter or admin role"); 24 | _; 25 | } 26 | 27 | function _initializeMinterRole(address minter) internal onlyInitializing { 28 | // Grant the role to a specified account 29 | _grantRole(MINTER_ROLE, minter); 30 | } 31 | 32 | /** 33 | * @notice Adds an account as an approved minter. 34 | * @dev Only callable by admins, as enforced by `grantRole`. 35 | * @param account The address to be approved. 36 | */ 37 | function grantMinter(address account) external { 38 | grantRole(MINTER_ROLE, account); 39 | } 40 | 41 | /** 42 | * @notice Removes an account from the set of approved minters. 43 | * @dev Only callable by admins, as enforced by `revokeRole`. 44 | * @param account The address to be removed. 45 | */ 46 | function revokeMinter(address account) external { 47 | revokeRole(MINTER_ROLE, account); 48 | } 49 | 50 | /** 51 | * @notice Checks if the account provided is an minter. 52 | * @param account The address to check. 53 | * @return approved True if the account is an minter. 54 | */ 55 | function isMinter(address account) public view returns (bool approved) { 56 | approved = hasRole(MINTER_ROLE, account); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /contracts/mixins/shared/Constants.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /// Constant values shared across mixins. 6 | 7 | /** 8 | * @dev 100% in basis points. 9 | */ 10 | uint256 constant BASIS_POINTS = 10_000; 11 | 12 | /** 13 | * @dev The default admin role defined by OZ ACL modules. 14 | */ 15 | bytes32 constant DEFAULT_ADMIN_ROLE = 0x00; 16 | 17 | /** 18 | * @dev Cap the number of royalty recipients. 19 | * A cap is required to ensure gas costs are not too high when a sale is settled. 20 | */ 21 | uint256 constant MAX_ROYALTY_RECIPIENTS = 5; 22 | 23 | /** 24 | * @dev The minimum increase of 10% required when making an offer or placing a bid. 25 | */ 26 | uint256 constant MIN_PERCENT_INCREMENT_DENOMINATOR = BASIS_POINTS / 1_000; 27 | 28 | /** 29 | * @dev The gas limit used when making external read-only calls. 30 | * This helps to ensure that external calls does not prevent the market from executing. 31 | */ 32 | uint256 constant READ_ONLY_GAS_LIMIT = 40_000; 33 | 34 | /** 35 | * @dev Default royalty cut paid out on secondary sales. 36 | * Set to 10% of the secondary sale. 37 | */ 38 | uint96 constant ROYALTY_IN_BASIS_POINTS = 1_000; 39 | 40 | /** 41 | * @dev 10%, expressed as a denominator for more efficient calculations. 42 | */ 43 | uint256 constant ROYALTY_RATIO = BASIS_POINTS / ROYALTY_IN_BASIS_POINTS; 44 | 45 | /** 46 | * @dev The gas limit to send ETH to multiple recipients, enough for a 5-way split. 47 | */ 48 | uint256 constant SEND_VALUE_GAS_LIMIT_MULTIPLE_RECIPIENTS = 210_000; 49 | 50 | /** 51 | * @dev The gas limit to send ETH to a single recipient, enough for a contract with a simple receiver. 52 | */ 53 | uint256 constant SEND_VALUE_GAS_LIMIT_SINGLE_RECIPIENT = 20_000; 54 | -------------------------------------------------------------------------------- /contracts/mixins/shared/ContractFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; 6 | 7 | /** 8 | * @title Stores a reference to the factory which is used to create contract proxies. 9 | * @author batu-inal & HardlyDifficult 10 | */ 11 | abstract contract ContractFactory { 12 | using AddressUpgradeable for address; 13 | 14 | /** 15 | * @notice The address of the factory which was used to create this contract. 16 | * @return The factory contract address. 17 | */ 18 | address public immutable contractFactory; 19 | 20 | modifier onlyContractFactory() { 21 | require(msg.sender == contractFactory, "ContractFactory: Caller is not the factory"); 22 | _; 23 | } 24 | 25 | /** 26 | * @notice Initialize the template's immutable variables. 27 | * @param _contractFactory The factory which will be used to create these contracts. 28 | */ 29 | constructor(address _contractFactory) { 30 | require(_contractFactory.isContract(), "ContractFactory: Factory is not a contract"); 31 | contractFactory = _contractFactory; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/mixins/shared/FETHNode.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; 6 | 7 | import "../../interfaces/internal/IFethMarket.sol"; 8 | 9 | error FETHNode_FETH_Address_Is_Not_A_Contract(); 10 | error FETHNode_Only_FETH_Can_Transfer_ETH(); 11 | 12 | /** 13 | * @title A mixin for interacting with the FETH contract. 14 | * @author batu-inal & HardlyDifficult 15 | */ 16 | abstract contract FETHNode { 17 | using AddressUpgradeable for address; 18 | using AddressUpgradeable for address payable; 19 | 20 | /// @notice The FETH ERC-20 token for managing escrow and lockup. 21 | IFethMarket internal immutable feth; 22 | 23 | constructor(address _feth) { 24 | if (!_feth.isContract()) { 25 | revert FETHNode_FETH_Address_Is_Not_A_Contract(); 26 | } 27 | 28 | feth = IFethMarket(_feth); 29 | } 30 | 31 | /** 32 | * @notice Only used by FETH. Any direct transfer from users will revert. 33 | */ 34 | receive() external payable { 35 | if (msg.sender != address(feth)) { 36 | revert FETHNode_Only_FETH_Can_Transfer_ETH(); 37 | } 38 | } 39 | 40 | /** 41 | * @notice Withdraw the msg.sender's available FETH balance if they requested more than the msg.value provided. 42 | * @dev This may revert if the msg.sender is non-receivable. 43 | * This helper should not be used anywhere that may lead to locked assets. 44 | * @param totalAmount The total amount of ETH required (including the msg.value). 45 | * @param shouldRefundSurplus If true, refund msg.value - totalAmount to the msg.sender. 46 | */ 47 | function _tryUseFETHBalance(uint256 totalAmount, bool shouldRefundSurplus) internal { 48 | if (totalAmount > msg.value) { 49 | // Withdraw additional ETH required from the user's available FETH balance. 50 | unchecked { 51 | // The if above ensures delta will not underflow. 52 | // Withdraw ETH from the user's account in the FETH token contract, 53 | // making the funds available in this contract as ETH. 54 | feth.marketWithdrawFrom(msg.sender, totalAmount - msg.value); 55 | } 56 | } else if (shouldRefundSurplus && totalAmount < msg.value) { 57 | // Return any surplus ETH to the user. 58 | unchecked { 59 | // The if above ensures this will not underflow 60 | payable(msg.sender).sendValue(msg.value - totalAmount); 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * @notice Gets the FETH contract used to escrow offer funds. 67 | * @return fethAddress The FETH contract address. 68 | */ 69 | function getFethAddress() external view returns (address fethAddress) { 70 | fethAddress = address(feth); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /contracts/mixins/shared/FoundationTreasuryNode.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; 6 | 7 | import "../../interfaces/internal/roles/IAdminRole.sol"; 8 | import "../../interfaces/internal/roles/IOperatorRole.sol"; 9 | 10 | error FoundationTreasuryNode_Address_Is_Not_A_Contract(); 11 | error FoundationTreasuryNode_Caller_Not_Admin(); 12 | error FoundationTreasuryNode_Caller_Not_Operator(); 13 | 14 | /** 15 | * @title A mixin that stores a reference to the Foundation treasury contract. 16 | * @notice The treasury collects fees and defines admin/operator roles. 17 | * @author batu-inal & HardlyDifficult 18 | */ 19 | abstract contract FoundationTreasuryNode { 20 | using AddressUpgradeable for address payable; 21 | 22 | /// @dev This value was replaced with an immutable version. 23 | address payable private __gap_was_treasury; 24 | 25 | /// @notice The address of the treasury contract. 26 | address payable private immutable treasury; 27 | 28 | /// @notice Requires the caller is a Foundation admin. 29 | modifier onlyFoundationAdmin() { 30 | if (!IAdminRole(treasury).isAdmin(msg.sender)) { 31 | revert FoundationTreasuryNode_Caller_Not_Admin(); 32 | } 33 | _; 34 | } 35 | 36 | /// @notice Requires the caller is a Foundation operator. 37 | modifier onlyFoundationOperator() { 38 | if (!IOperatorRole(treasury).isOperator(msg.sender)) { 39 | revert FoundationTreasuryNode_Caller_Not_Operator(); 40 | } 41 | _; 42 | } 43 | 44 | /** 45 | * @notice Set immutable variables for the implementation contract. 46 | * @dev Assigns the treasury contract address. 47 | */ 48 | constructor(address payable _treasury) { 49 | if (!_treasury.isContract()) { 50 | revert FoundationTreasuryNode_Address_Is_Not_A_Contract(); 51 | } 52 | treasury = _treasury; 53 | } 54 | 55 | /** 56 | * @notice Gets the Foundation treasury contract. 57 | * @dev This call is used in the royalty registry contract. 58 | * @return treasuryAddress The address of the Foundation treasury contract. 59 | */ 60 | function getFoundationTreasury() public view returns (address payable treasuryAddress) { 61 | treasuryAddress = treasury; 62 | } 63 | 64 | /** 65 | * @notice This empty reserved space is put in place to allow future versions to add new 66 | * variables without shifting down storage in the inheritance chain. 67 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 68 | */ 69 | uint256[2_000] private __gap; 70 | } 71 | -------------------------------------------------------------------------------- /contracts/mixins/shared/Gap10000.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | /** 6 | * @title A placeholder contract leaving room for new mixins to be added to the future. 7 | * @author batu-inal & HardlyDifficult 8 | */ 9 | abstract contract Gap10000 { 10 | /** 11 | * @notice This empty reserved space is put in place to allow future versions to add new 12 | * variables without shifting down storage in the inheritance chain. 13 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 14 | */ 15 | uint256[10_000] private __gap; 16 | } 17 | -------------------------------------------------------------------------------- /contracts/mixins/shared/MarketSharedCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "./FETHNode.sol"; 6 | 7 | /** 8 | * @title A place for common modifiers and functions used by various market mixins, if any. 9 | * @dev This also leaves a gap which can be used to add a new mixin to the top of the inheritance tree. 10 | * @author batu-inal & HardlyDifficult 11 | */ 12 | abstract contract MarketSharedCore is FETHNode { 13 | /** 14 | * @notice Checks who the seller for an NFT is if listed in this market. 15 | * @param nftContract The address of the NFT contract. 16 | * @param tokenId The id of the NFT. 17 | * @return seller The seller which listed this NFT for sale, or address(0) if not listed. 18 | */ 19 | function getSellerOf(address nftContract, uint256 tokenId) external view returns (address payable seller) { 20 | seller = _getSellerOf(nftContract, tokenId); 21 | } 22 | 23 | /** 24 | * @notice Checks who the seller for an NFT is if listed in this market. 25 | */ 26 | function _getSellerOf(address nftContract, uint256 tokenId) internal view virtual returns (address payable seller); 27 | 28 | /** 29 | * @notice Checks who the seller for an NFT is if listed in this market or returns the current owner. 30 | */ 31 | function _getSellerOrOwnerOf(address nftContract, uint256 tokenId) 32 | internal 33 | view 34 | virtual 35 | returns (address payable sellerOrOwner); 36 | 37 | /** 38 | * @notice This empty reserved space is put in place to allow future versions to add new 39 | * variables without shifting down storage in the inheritance chain. 40 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 41 | */ 42 | uint256[500] private __gap; 43 | } 44 | -------------------------------------------------------------------------------- /contracts/mixins/shared/SendValueWithFallbackWithdraw.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; 6 | 7 | import "./FETHNode.sol"; 8 | 9 | /** 10 | * @title A mixin for sending ETH with a fallback withdraw mechanism. 11 | * @notice Attempt to send ETH and if the transfer fails or runs out of gas, store the balance 12 | * in the FETH token contract for future withdrawal instead. 13 | * @dev This mixin was recently switched to escrow funds in FETH. 14 | * Once we have confirmed all pending balances have been withdrawn, we can remove the escrow tracking here. 15 | * @author batu-inal & HardlyDifficult 16 | */ 17 | abstract contract SendValueWithFallbackWithdraw is FETHNode { 18 | using AddressUpgradeable for address payable; 19 | 20 | /// @dev Removing old unused variables in an upgrade safe way. 21 | uint256 private __gap_was_pendingWithdrawals; 22 | 23 | /** 24 | * @notice Emitted when escrowed funds are withdrawn to FETH. 25 | * @param user The account which has withdrawn ETH. 26 | * @param amount The amount of ETH which has been withdrawn. 27 | */ 28 | event WithdrawalToFETH(address indexed user, uint256 amount); 29 | 30 | /** 31 | * @notice Attempt to send a user or contract ETH. 32 | * If it fails store the amount owned for later withdrawal in FETH. 33 | * @dev This may fail when sending ETH to a contract that is non-receivable or exceeds the gas limit specified. 34 | */ 35 | function _sendValueWithFallbackWithdraw( 36 | address payable user, 37 | uint256 amount, 38 | uint256 gasLimit 39 | ) internal { 40 | if (amount == 0) { 41 | return; 42 | } 43 | // Cap the gas to prevent consuming all available gas to block a tx from completing successfully 44 | // solhint-disable-next-line avoid-low-level-calls 45 | (bool success, ) = user.call{ value: amount, gas: gasLimit }(""); 46 | if (!success) { 47 | // Store the funds that failed to send for the user in the FETH token 48 | feth.depositFor{ value: amount }(user); 49 | emit WithdrawalToFETH(user, amount); 50 | } 51 | } 52 | 53 | /** 54 | * @notice This empty reserved space is put in place to allow future versions to add new 55 | * variables without shifting down storage in the inheritance chain. 56 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 57 | */ 58 | uint256[999] private __gap; 59 | } 60 | -------------------------------------------------------------------------------- /contracts/mixins/treasury/AdminRoleEnumerable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 6 | 7 | import "./OZAccessControlUpgradeable.sol"; 8 | 9 | /** 10 | * @title Defines a role for Foundation admin accounts. 11 | * @dev Wraps the default admin role from OpenZeppelin's AccessControl for easy integration. 12 | * @author batu-inal & HardlyDifficult 13 | */ 14 | abstract contract AdminRoleEnumerable is Initializable, OZAccessControlUpgradeable { 15 | modifier onlyAdmin() { 16 | require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "AdminRole: caller does not have the Admin role"); 17 | _; 18 | } 19 | 20 | function _initializeAdminRole(address admin) internal onlyInitializing { 21 | // Grant the role to a specified account 22 | _setupRole(DEFAULT_ADMIN_ROLE, admin); 23 | } 24 | 25 | /** 26 | * @notice Adds the account to the list of approved admins. 27 | * @dev Only callable by admins as enforced by `grantRole`. 28 | * @param account The address to be approved. 29 | */ 30 | function grantAdmin(address account) external { 31 | grantRole(DEFAULT_ADMIN_ROLE, account); 32 | } 33 | 34 | /** 35 | * @notice Removes the account from the list of approved admins. 36 | * @dev Only callable by admins as enforced by `revokeRole`. 37 | * @param account The address to be removed from the approved list. 38 | */ 39 | function revokeAdmin(address account) external { 40 | revokeRole(DEFAULT_ADMIN_ROLE, account); 41 | } 42 | 43 | /** 44 | * @notice Returns one of the admins by index. 45 | * @param index The index of the admin to return from 0 to getAdminMemberCount() - 1. 46 | * @return account The address of the admin. 47 | */ 48 | function getAdminMember(uint256 index) external view returns (address account) { 49 | account = getRoleMember(DEFAULT_ADMIN_ROLE, index); 50 | } 51 | 52 | /** 53 | * @notice Checks how many accounts have been granted admin access. 54 | * @return count The number of accounts with admin access. 55 | */ 56 | function getAdminMemberCount() external view returns (uint256 count) { 57 | count = getRoleMemberCount(DEFAULT_ADMIN_ROLE); 58 | } 59 | 60 | /** 61 | * @notice Checks if the account provided is an admin. 62 | * @param account The address to check. 63 | * @return approved True if the account is an admin. 64 | * @dev This call is used by the royalty registry contract. 65 | */ 66 | function isAdmin(address account) external view returns (bool approved) { 67 | approved = hasRole(DEFAULT_ADMIN_ROLE, account); 68 | } 69 | 70 | /** 71 | * @notice This empty reserved space is put in place to allow future versions to add new 72 | * variables without shifting down storage in the inheritance chain. 73 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 74 | */ 75 | uint256[1_000] private __gap; 76 | } 77 | -------------------------------------------------------------------------------- /contracts/mixins/treasury/CollateralManagement.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; 6 | 7 | import "./AdminRoleEnumerable.sol"; 8 | 9 | error CollateralManagement_Cannot_Withdraw_To_Address_Zero(); 10 | error CollateralManagement_Cannot_Withdraw_To_Self(); 11 | 12 | /** 13 | * @title Enables deposits and withdrawals. 14 | * @author batu-inal & HardlyDifficult 15 | */ 16 | abstract contract CollateralManagement is AdminRoleEnumerable { 17 | using AddressUpgradeable for address payable; 18 | 19 | /** 20 | * @notice Emitted when funds are withdrawn from this contract. 21 | * @param to The address which received the ETH withdrawn. 22 | * @param amount The amount of ETH which was withdrawn. 23 | */ 24 | event FundsWithdrawn(address indexed to, uint256 amount); 25 | 26 | /** 27 | * @notice Accept native currency payments (i.e. fees) 28 | */ 29 | // solhint-disable-next-line no-empty-blocks 30 | receive() external payable {} 31 | 32 | /** 33 | * @notice Allows an admin to withdraw funds. 34 | * @param to Address to receive the withdrawn funds 35 | * @param amount Amount to withdrawal or 0 to withdraw all available funds 36 | */ 37 | function withdrawFunds(address payable to, uint256 amount) external onlyAdmin { 38 | if (amount == 0) { 39 | amount = address(this).balance; 40 | } 41 | if (to == address(0)) { 42 | revert CollateralManagement_Cannot_Withdraw_To_Address_Zero(); 43 | } else if (to == address(this)) { 44 | revert CollateralManagement_Cannot_Withdraw_To_Self(); 45 | } 46 | to.sendValue(amount); 47 | 48 | emit FundsWithdrawn(to, amount); 49 | } 50 | 51 | /** 52 | * @notice This empty reserved space is put in place to allow future versions to add new 53 | * variables without shifting down storage in the inheritance chain. 54 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 55 | */ 56 | uint256[1_000] private __gap; 57 | } 58 | -------------------------------------------------------------------------------- /contracts/mixins/treasury/OZAccessControlUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | /** 4 | * Copied from https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts-upgradeable/v3.4.2-solc-0.7 5 | * Modified to support solc-8. 6 | * Using this instead of the new OZ implementation due to a change in storage slots used. 7 | * Also limited access of several functions as we will be using convenience wrappers. 8 | */ 9 | 10 | pragma solidity ^0.8.12; 11 | 12 | import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 13 | import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; 14 | import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; 15 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 16 | 17 | // solhint-disable 18 | 19 | /** 20 | * @title Implements role-based access control mechanisms. 21 | * @dev Contract module that allows children to implement role-based access 22 | * control mechanisms. 23 | * 24 | * Roles are referred to by their `bytes32` identifier. These should be exposed 25 | * in the external API and be unique. The best way to achieve this is by 26 | * using `public constant` hash digests: 27 | * 28 | * ``` 29 | * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); 30 | * ``` 31 | * 32 | * Roles can be used to represent a set of permissions. To restrict access to a 33 | * function call, use {hasRole}: 34 | * 35 | * ``` 36 | * function foo() public { 37 | * require(hasRole(MY_ROLE, msg.sender)); 38 | * ... 39 | * } 40 | * ``` 41 | * 42 | * Roles can be granted and revoked dynamically via the {grantRole} and 43 | * {revokeRole} functions. Each role has an associated admin role, and only 44 | * accounts that have a role's admin role can call {grantRole} and {revokeRole}. 45 | * 46 | * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means 47 | * that only accounts with this role will be able to grant or revoke other 48 | * roles. More complex role relationships can be created by using 49 | * {_setRoleAdmin}. 50 | * 51 | * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to 52 | * grant and revoke this role. Extra precautions should be taken to secure 53 | * accounts that have been granted it. 54 | */ 55 | abstract contract OZAccessControlUpgradeable is Initializable, ContextUpgradeable { 56 | using EnumerableSet for EnumerableSet.AddressSet; 57 | using AddressUpgradeable for address; 58 | 59 | struct RoleData { 60 | EnumerableSet.AddressSet members; 61 | bytes32 adminRole; 62 | } 63 | 64 | mapping(bytes32 => RoleData) private _roles; 65 | 66 | bytes32 internal constant DEFAULT_ADMIN_ROLE = 0x00; 67 | 68 | /** 69 | * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` 70 | * 71 | * `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite 72 | * {RoleAdminChanged} not being emitted signaling this. 73 | * 74 | * _Available since v3.1._ 75 | */ 76 | event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole); 77 | 78 | /** 79 | * @dev Emitted when `account` is granted `role`. 80 | * 81 | * `sender` is the account that originated the contract call, an admin role 82 | * bearer except when using {_setupRole}. 83 | */ 84 | event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); 85 | 86 | /** 87 | * @dev Emitted when `account` is revoked `role`. 88 | * 89 | * `sender` is the account that originated the contract call: 90 | * - if using `revokeRole`, it is the admin role bearer 91 | * - if using `renounceRole`, it is the role bearer (i.e. `account`) 92 | */ 93 | event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender); 94 | 95 | /** 96 | * @dev Returns `true` if `account` has been granted `role`. 97 | */ 98 | function hasRole(bytes32 role, address account) internal view returns (bool) { 99 | return _roles[role].members.contains(account); 100 | } 101 | 102 | /** 103 | * @dev Returns the number of accounts that have `role`. Can be used 104 | * together with {getRoleMember} to enumerate all bearers of a role. 105 | */ 106 | function getRoleMemberCount(bytes32 role) internal view returns (uint256) { 107 | return _roles[role].members.length(); 108 | } 109 | 110 | /** 111 | * @dev Returns one of the accounts that have `role`. `index` must be a 112 | * value between 0 and {getRoleMemberCount}, non-inclusive. 113 | * 114 | * Role bearers are not sorted in any particular way, and their ordering may 115 | * change at any point. 116 | * 117 | * WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure 118 | * you perform all queries on the same block. See the following 119 | * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] 120 | * for more information. 121 | */ 122 | function getRoleMember(bytes32 role, uint256 index) internal view returns (address) { 123 | return _roles[role].members.at(index); 124 | } 125 | 126 | /** 127 | * @dev Grants `role` to `account`. 128 | * 129 | * If `account` had not been already granted `role`, emits a {RoleGranted} 130 | * event. 131 | * 132 | * Requirements: 133 | * 134 | * - the caller must have ``role``'s admin role. 135 | */ 136 | function grantRole(bytes32 role, address account) internal virtual { 137 | require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to grant"); 138 | 139 | _grantRole(role, account); 140 | } 141 | 142 | /** 143 | * @dev Revokes `role` from `account`. 144 | * 145 | * If `account` had been granted `role`, emits a {RoleRevoked} event. 146 | * 147 | * Requirements: 148 | * 149 | * - the caller must have ``role``'s admin role. 150 | */ 151 | function revokeRole(bytes32 role, address account) internal virtual { 152 | require(hasRole(_roles[role].adminRole, _msgSender()), "AccessControl: sender must be an admin to revoke"); 153 | 154 | _revokeRole(role, account); 155 | } 156 | 157 | /** 158 | * @dev Grants `role` to `account`. 159 | * 160 | * If `account` had not been already granted `role`, emits a {RoleGranted} 161 | * event. Note that unlike {grantRole}, this function doesn't perform any 162 | * checks on the calling account. 163 | * 164 | * [WARNING] 165 | * ==== 166 | * This function should only be called from the constructor when setting 167 | * up the initial roles for the system. 168 | * 169 | * Using this function in any other way is effectively circumventing the admin 170 | * system imposed by {AccessControl}. 171 | * ==== 172 | */ 173 | function _setupRole(bytes32 role, address account) internal { 174 | _grantRole(role, account); 175 | } 176 | 177 | function _grantRole(bytes32 role, address account) private { 178 | if (_roles[role].members.add(account)) { 179 | emit RoleGranted(role, account, _msgSender()); 180 | } 181 | } 182 | 183 | function _revokeRole(bytes32 role, address account) private { 184 | if (_roles[role].members.remove(account)) { 185 | emit RoleRevoked(role, account, _msgSender()); 186 | } 187 | } 188 | 189 | /** 190 | * @notice This empty reserved space is put in place to allow future versions to add new 191 | * variables without shifting down storage in the inheritance chain. 192 | * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps 193 | */ 194 | uint256[49] private __gap; 195 | } 196 | -------------------------------------------------------------------------------- /contracts/mixins/treasury/OperatorRoleEnumerable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | pragma solidity ^0.8.12; 4 | 5 | import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; 6 | 7 | import "./OZAccessControlUpgradeable.sol"; 8 | 9 | /** 10 | * @title Defines a role for Foundation operator accounts. 11 | * @dev Wraps a role from OpenZeppelin's AccessControl for easy integration. 12 | * @author batu-inal & HardlyDifficult 13 | */ 14 | abstract contract OperatorRoleEnumerable is Initializable, OZAccessControlUpgradeable { 15 | bytes32 private constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); 16 | 17 | /** 18 | * @notice Adds the account to the list of approved operators. 19 | * @dev Only callable by admins as enforced by `grantRole`. 20 | * @param account The address to be approved. 21 | */ 22 | function grantOperator(address account) external { 23 | grantRole(OPERATOR_ROLE, account); 24 | } 25 | 26 | /** 27 | * @notice Removes the account from the list of approved operators. 28 | * @dev Only callable by admins as enforced by `revokeRole`. 29 | * @param account The address to be removed from the approved list. 30 | */ 31 | function revokeOperator(address account) external { 32 | revokeRole(OPERATOR_ROLE, account); 33 | } 34 | 35 | /** 36 | * @notice Returns one of the operator by index. 37 | * @param index The index of the operator to return from 0 to getOperatorMemberCount() - 1. 38 | * @return account The address of the operator. 39 | */ 40 | function getOperatorMember(uint256 index) external view returns (address account) { 41 | account = getRoleMember(OPERATOR_ROLE, index); 42 | } 43 | 44 | /** 45 | * @notice Checks how many accounts have been granted operator access. 46 | * @return count The number of accounts with operator access. 47 | */ 48 | function getOperatorMemberCount() external view returns (uint256 count) { 49 | count = getRoleMemberCount(OPERATOR_ROLE); 50 | } 51 | 52 | /** 53 | * @notice Checks if the account provided is an operator. 54 | * @param account The address to check. 55 | * @return approved True if the account is an operator. 56 | */ 57 | function isOperator(address account) external view returns (bool approved) { 58 | approved = hasRole(OPERATOR_ROLE, account); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs/FNDMiddleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FNDMiddleware 3 | description: Convenience methods to ease integration with other contracts. 4 | --- 5 | 6 | 7 | This will aggregate calls and format the output per the needs of our frontend or other consumers. 8 | 9 | 10 | 11 | ## Methods 12 | 13 | ### getAccountInfo 14 | 15 | ```solidity 16 | function getAccountInfo(address account) external view returns (uint256 ethBalance, uint256 availableFethBalance, uint256 lockedFethBalance, string ensName) 17 | ``` 18 | 19 | 20 | 21 | 22 | 23 | **Parameters** 24 | 25 | | Name | Type | Description | 26 | |---|---|---| 27 | | account | `address` | | 28 | 29 | **Returns** 30 | 31 | | Name | Type | Description | 32 | |---|---|---| 33 | | ethBalance | `uint256` | | 34 | | availableFethBalance | `uint256` | | 35 | | lockedFethBalance | `uint256` | | 36 | | ensName | `string` | | 37 | 38 | ### getFees 39 | 40 | ```solidity 41 | function getFees(address nftContract, uint256 tokenId, uint256 price) external view returns (struct FNDMiddleware.FeeWithRecipient protocol, struct FNDMiddleware.Fee creator, struct FNDMiddleware.FeeWithRecipient owner, struct FNDMiddleware.RevSplit[] creatorRevSplit) 42 | ``` 43 | 44 | 45 | 46 | 47 | 48 | **Parameters** 49 | 50 | | Name | Type | Description | 51 | |---|---|---| 52 | | nftContract | `address` | | 53 | | tokenId | `uint256` | | 54 | | price | `uint256` | | 55 | 56 | **Returns** 57 | 58 | | Name | Type | Description | 59 | |---|---|---| 60 | | protocol | `FNDMiddleware.FeeWithRecipient` | | 61 | | creator | `FNDMiddleware.Fee` | | 62 | | owner | `FNDMiddleware.FeeWithRecipient` | | 63 | | creatorRevSplit | `FNDMiddleware.RevSplit[]` | | 64 | 65 | ### getNFTDetailString 66 | 67 | ```solidity 68 | function getNFTDetailString(address nftContract, uint256 tokenId) external view returns (string details) 69 | ``` 70 | 71 | Retrieves details about the current state of an NFT in the FND Market as a string. 72 | 73 | *This API is for investigations & convenience, it is not meant to be consumed by an app directly. Future upgrades may not be backwards compatible.* 74 | 75 | **Parameters** 76 | 77 | | Name | Type | Description | 78 | |---|---|---| 79 | | nftContract | `address` | | 80 | | tokenId | `uint256` | | 81 | 82 | **Returns** 83 | 84 | | Name | Type | Description | 85 | |---|---|---| 86 | | details | `string` | | 87 | 88 | ### getNFTDetails 89 | 90 | ```solidity 91 | function getNFTDetails(address nftContract, uint256 tokenId) external view returns (address owner, bool isInEscrow, address auctionBidder, uint256 auctionEndTime, uint256 auctionPrice, uint256 auctionId, uint256 buyPrice, uint256 offerAmount, address offerBuyer, uint256 offerExpiration) 92 | ``` 93 | 94 | Retrieves details about the current state of an NFT in the FND Market. 95 | 96 | 97 | 98 | **Parameters** 99 | 100 | | Name | Type | Description | 101 | |---|---|---| 102 | | nftContract | `address` | The address of the NFT contract. | 103 | | tokenId | `uint256` | The id of the NFT. | 104 | 105 | **Returns** 106 | 107 | | Name | Type | Description | 108 | |---|---|---| 109 | | owner | `address` | The account which currently holds the NFT or has listed it for sale. | 110 | | isInEscrow | `bool` | True if the NFT is currently held in escrow by the Market (for an auction or buy price). | 111 | | auctionBidder | `address` | The current highest bidder for the auction, or address(0) if there's not an active auction. | 112 | | auctionEndTime | `uint256` | The time at which this auction will not accept any new bids, this is `0` until the first bid is placed. | 113 | | auctionPrice | `uint256` | The latest price of the NFT in this auction. This is set to the reserve price, and then to the highest bid once the auction has started. Returns `0` if there's no auction for this NFT. | 114 | | auctionId | `uint256` | The id of the auction, or 0 if no auction is found. | 115 | | buyPrice | `uint256` | The price at which you could buy this NFT. Returns max uint256 if there is no buy price set for this NFT (since a price of 0 is supported). | 116 | | offerAmount | `uint256` | The amount being offered for this NFT. Returns `0` if there is no offer or the most recent offer has expired. | 117 | | offerBuyer | `address` | The address of the buyer that made the current highest offer. Returns `address(0)` if there is no offer or the most recent offer has expired. | 118 | | offerExpiration | `uint256` | The timestamp that the current highest offer expires. Returns `0` if there is no offer or the most recent offer has expired. | 119 | 120 | ### getSellerOrOwnerOf 121 | 122 | ```solidity 123 | function getSellerOrOwnerOf(address nftContract, uint256 tokenId) external view returns (address payable ownerOrSeller) 124 | ``` 125 | 126 | Checks who the seller for an NFT is, checking both markets or returning the current owner. 127 | 128 | 129 | 130 | **Parameters** 131 | 132 | | Name | Type | Description | 133 | |---|---|---| 134 | | nftContract | `address` | | 135 | | tokenId | `uint256` | | 136 | 137 | **Returns** 138 | 139 | | Name | Type | Description | 140 | |---|---|---| 141 | | ownerOrSeller | `address payable` | | 142 | 143 | ### getSplitShareLength 144 | 145 | ```solidity 146 | function getSplitShareLength(address payable recipient) external view returns (uint256 count) 147 | ``` 148 | 149 | 150 | 151 | 152 | 153 | **Parameters** 154 | 155 | | Name | Type | Description | 156 | |---|---|---| 157 | | recipient | `address payable` | | 158 | 159 | **Returns** 160 | 161 | | Name | Type | Description | 162 | |---|---|---| 163 | | count | `uint256` | | 164 | 165 | ### getTokenCreator 166 | 167 | ```solidity 168 | function getTokenCreator(address nftContract, uint256 tokenId) external view returns (address creatorAddress) 169 | ``` 170 | 171 | 172 | 173 | 174 | 175 | **Parameters** 176 | 177 | | Name | Type | Description | 178 | |---|---|---| 179 | | nftContract | `address` | | 180 | | tokenId | `uint256` | | 181 | 182 | **Returns** 183 | 184 | | Name | Type | Description | 185 | |---|---|---| 186 | | creatorAddress | `address` | | 187 | 188 | ### probeNFT 189 | 190 | ```solidity 191 | function probeNFT(address nftContract, uint256 tokenId) external payable returns (bytes32) 192 | ``` 193 | 194 | Checks an NFT to confirm it will function correctly with our marketplace. 195 | 196 | *This should be called with as `call` to simulate the tx; never `sendTransaction`.* 197 | 198 | **Parameters** 199 | 200 | | Name | Type | Description | 201 | |---|---|---| 202 | | nftContract | `address` | | 203 | | tokenId | `uint256` | | 204 | 205 | **Returns** 206 | 207 | | Name | Type | Description | 208 | |---|---|---| 209 | | _0 | `bytes32` | 0 if the NFT is supported, otherwise a hash of the error reason. | 210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /docs/FoundationTreasury.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FoundationTreasury 3 | description: Manage revenue and roles for Foundation. 4 | --- 5 | 6 | 7 | All fees generated by the market are forwarded to this contract. It also defines the Admin and Operator roles which are used in other contracts. 8 | 9 | 10 | 11 | ## Methods 12 | 13 | ### getAdminMember 14 | 15 | ```solidity 16 | function getAdminMember(uint256 index) external view returns (address account) 17 | ``` 18 | 19 | Returns one of the admins by index. 20 | 21 | 22 | 23 | **Parameters** 24 | 25 | | Name | Type | Description | 26 | |---|---|---| 27 | | index | `uint256` | The index of the admin to return from 0 to getAdminMemberCount() - 1. | 28 | 29 | **Returns** 30 | 31 | | Name | Type | Description | 32 | |---|---|---| 33 | | account | `address` | The address of the admin. | 34 | 35 | ### getAdminMemberCount 36 | 37 | ```solidity 38 | function getAdminMemberCount() external view returns (uint256 count) 39 | ``` 40 | 41 | Checks how many accounts have been granted admin access. 42 | 43 | 44 | 45 | 46 | **Returns** 47 | 48 | | Name | Type | Description | 49 | |---|---|---| 50 | | count | `uint256` | The number of accounts with admin access. | 51 | 52 | ### getOperatorMember 53 | 54 | ```solidity 55 | function getOperatorMember(uint256 index) external view returns (address account) 56 | ``` 57 | 58 | Returns one of the operator by index. 59 | 60 | 61 | 62 | **Parameters** 63 | 64 | | Name | Type | Description | 65 | |---|---|---| 66 | | index | `uint256` | The index of the operator to return from 0 to getOperatorMemberCount() - 1. | 67 | 68 | **Returns** 69 | 70 | | Name | Type | Description | 71 | |---|---|---| 72 | | account | `address` | The address of the operator. | 73 | 74 | ### getOperatorMemberCount 75 | 76 | ```solidity 77 | function getOperatorMemberCount() external view returns (uint256 count) 78 | ``` 79 | 80 | Checks how many accounts have been granted operator access. 81 | 82 | 83 | 84 | 85 | **Returns** 86 | 87 | | Name | Type | Description | 88 | |---|---|---| 89 | | count | `uint256` | The number of accounts with operator access. | 90 | 91 | ### grantAdmin 92 | 93 | ```solidity 94 | function grantAdmin(address account) external nonpayable 95 | ``` 96 | 97 | Adds the account to the list of approved admins. 98 | 99 | *Only callable by admins as enforced by `grantRole`.* 100 | 101 | **Parameters** 102 | 103 | | Name | Type | Description | 104 | |---|---|---| 105 | | account | `address` | The address to be approved. | 106 | 107 | ### grantOperator 108 | 109 | ```solidity 110 | function grantOperator(address account) external nonpayable 111 | ``` 112 | 113 | Adds the account to the list of approved operators. 114 | 115 | *Only callable by admins as enforced by `grantRole`.* 116 | 117 | **Parameters** 118 | 119 | | Name | Type | Description | 120 | |---|---|---| 121 | | account | `address` | The address to be approved. | 122 | 123 | ### initialize 124 | 125 | ```solidity 126 | function initialize(address admin) external nonpayable 127 | ``` 128 | 129 | Called one time after deployment to initialize the contract. 130 | 131 | 132 | 133 | **Parameters** 134 | 135 | | Name | Type | Description | 136 | |---|---|---| 137 | | admin | `address` | The account to add as the initial admin. | 138 | 139 | ### isAdmin 140 | 141 | ```solidity 142 | function isAdmin(address account) external view returns (bool approved) 143 | ``` 144 | 145 | Checks if the account provided is an admin. 146 | 147 | *This call is used by the royalty registry contract.* 148 | 149 | **Parameters** 150 | 151 | | Name | Type | Description | 152 | |---|---|---| 153 | | account | `address` | The address to check. | 154 | 155 | **Returns** 156 | 157 | | Name | Type | Description | 158 | |---|---|---| 159 | | approved | `bool` | True if the account is an admin. | 160 | 161 | ### isOperator 162 | 163 | ```solidity 164 | function isOperator(address account) external view returns (bool approved) 165 | ``` 166 | 167 | Checks if the account provided is an operator. 168 | 169 | 170 | 171 | **Parameters** 172 | 173 | | Name | Type | Description | 174 | |---|---|---| 175 | | account | `address` | The address to check. | 176 | 177 | **Returns** 178 | 179 | | Name | Type | Description | 180 | |---|---|---| 181 | | approved | `bool` | True if the account is an operator. | 182 | 183 | ### revokeAdmin 184 | 185 | ```solidity 186 | function revokeAdmin(address account) external nonpayable 187 | ``` 188 | 189 | Removes the account from the list of approved admins. 190 | 191 | *Only callable by admins as enforced by `revokeRole`.* 192 | 193 | **Parameters** 194 | 195 | | Name | Type | Description | 196 | |---|---|---| 197 | | account | `address` | The address to be removed from the approved list. | 198 | 199 | ### revokeOperator 200 | 201 | ```solidity 202 | function revokeOperator(address account) external nonpayable 203 | ``` 204 | 205 | Removes the account from the list of approved operators. 206 | 207 | *Only callable by admins as enforced by `revokeRole`.* 208 | 209 | **Parameters** 210 | 211 | | Name | Type | Description | 212 | |---|---|---| 213 | | account | `address` | The address to be removed from the approved list. | 214 | 215 | ### withdrawFunds 216 | 217 | ```solidity 218 | function withdrawFunds(address payable to, uint256 amount) external nonpayable 219 | ``` 220 | 221 | Allows an admin to withdraw funds. 222 | 223 | 224 | 225 | **Parameters** 226 | 227 | | Name | Type | Description | 228 | |---|---|---| 229 | | to | `address payable` | Address to receive the withdrawn funds | 230 | | amount | `uint256` | Amount to withdrawal or 0 to withdraw all available funds | 231 | 232 | 233 | 234 | ## Events 235 | 236 | ### FundsWithdrawn 237 | 238 | ```solidity 239 | event FundsWithdrawn(address indexed to, uint256 amount) 240 | ``` 241 | 242 | Emitted when funds are withdrawn from this contract. 243 | 244 | 245 | 246 | **Parameters** 247 | 248 | | Name | Type | Description | 249 | |---|---|---| 250 | | to `indexed` | `address` | | 251 | | amount | `uint256` | | 252 | 253 | ### Initialized 254 | 255 | ```solidity 256 | event Initialized(uint8 version) 257 | ``` 258 | 259 | 260 | 261 | 262 | 263 | **Parameters** 264 | 265 | | Name | Type | Description | 266 | |---|---|---| 267 | | version | `uint8` | | 268 | 269 | ### RoleAdminChanged 270 | 271 | ```solidity 272 | event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) 273 | ``` 274 | 275 | 276 | 277 | 278 | 279 | **Parameters** 280 | 281 | | Name | Type | Description | 282 | |---|---|---| 283 | | role `indexed` | `bytes32` | | 284 | | previousAdminRole `indexed` | `bytes32` | | 285 | | newAdminRole `indexed` | `bytes32` | | 286 | 287 | ### RoleGranted 288 | 289 | ```solidity 290 | event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) 291 | ``` 292 | 293 | 294 | 295 | 296 | 297 | **Parameters** 298 | 299 | | Name | Type | Description | 300 | |---|---|---| 301 | | role `indexed` | `bytes32` | | 302 | | account `indexed` | `address` | | 303 | | sender `indexed` | `address` | | 304 | 305 | ### RoleRevoked 306 | 307 | ```solidity 308 | event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) 309 | ``` 310 | 311 | 312 | 313 | 314 | 315 | **Parameters** 316 | 317 | | Name | Type | Description | 318 | |---|---|---| 319 | | role `indexed` | `bytes32` | | 320 | | account `indexed` | `address` | | 321 | | sender `indexed` | `address` | | 322 | 323 | 324 | 325 | ## Errors 326 | 327 | ### CollateralManagement_Cannot_Withdraw_To_Address_Zero 328 | 329 | ```solidity 330 | error CollateralManagement_Cannot_Withdraw_To_Address_Zero() 331 | ``` 332 | 333 | 334 | 335 | 336 | 337 | 338 | ### CollateralManagement_Cannot_Withdraw_To_Self 339 | 340 | ```solidity 341 | error CollateralManagement_Cannot_Withdraw_To_Self() 342 | ``` 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | -------------------------------------------------------------------------------- /docs/NFTCollectionFactory.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NFTCollectionFactory 3 | description: A factory to create NFT collections. 4 | --- 5 | 6 | 7 | Call this factory to create NFT collections. 8 | 9 | *This creates and initializes an ERC-1167 minimal proxy pointing to an NFT collection contract implementation.* 10 | 11 | ## Methods 12 | 13 | ### adminUpdateNFTCollectionImplementation 14 | 15 | ```solidity 16 | function adminUpdateNFTCollectionImplementation(address _implementation) external nonpayable 17 | ``` 18 | 19 | Allows Foundation to change the NFTCollection implementation used for future collections. This call will auto-increment the version. Existing collections are not impacted. 20 | 21 | 22 | 23 | **Parameters** 24 | 25 | | Name | Type | Description | 26 | |---|---|---| 27 | | _implementation | `address` | The new NFTCollection collection implementation address. | 28 | 29 | ### adminUpdateNFTDropCollectionImplementation 30 | 31 | ```solidity 32 | function adminUpdateNFTDropCollectionImplementation(address _implementation) external nonpayable 33 | ``` 34 | 35 | Allows Foundation to change the NFTDropCollection implementation used for future collections. This call will auto-increment the version. Existing collections are not impacted. 36 | 37 | 38 | 39 | **Parameters** 40 | 41 | | Name | Type | Description | 42 | |---|---|---| 43 | | _implementation | `address` | The new NFTDropCollection collection implementation address. | 44 | 45 | ### createNFTCollection 46 | 47 | ```solidity 48 | function createNFTCollection(string name, string symbol, uint96 nonce) external nonpayable returns (address collection) 49 | ``` 50 | 51 | Create a new collection contract. 52 | 53 | *The nonce must be unique for the msg.sender + implementation version, otherwise this call will revert.* 54 | 55 | **Parameters** 56 | 57 | | Name | Type | Description | 58 | |---|---|---| 59 | | name | `string` | The collection's `name`. | 60 | | symbol | `string` | The collection's `symbol`. | 61 | | nonce | `uint96` | An arbitrary value used to allow a creator to mint multiple collections with a counterfactual address. | 62 | 63 | **Returns** 64 | 65 | | Name | Type | Description | 66 | |---|---|---| 67 | | collection | `address` | The address of the newly created collection contract. | 68 | 69 | ### createNFTDropCollection 70 | 71 | ```solidity 72 | function createNFTDropCollection(string name, string symbol, string baseURI, bool isRevealed, uint32 maxTokenId, address approvedMinter, uint96 nonce) external nonpayable returns (address collection) 73 | ``` 74 | 75 | Create a new drop collection contract. 76 | 77 | *The nonce must be unique for the msg.sender + implementation version, otherwise this call will revert.* 78 | 79 | **Parameters** 80 | 81 | | Name | Type | Description | 82 | |---|---|---| 83 | | name | `string` | The collection's `name`. | 84 | | symbol | `string` | The collection's `symbol`. | 85 | | baseURI | `string` | The base URI for the collection. | 86 | | isRevealed | `bool` | Whether the collection is revealed or not. | 87 | | maxTokenId | `uint32` | The max token id for this collection. | 88 | | approvedMinter | `address` | An optional address to grant the MINTER_ROLE. | 89 | | nonce | `uint96` | An arbitrary value used to allow a creator to mint multiple collections with a counterfactual address. | 90 | 91 | **Returns** 92 | 93 | | Name | Type | Description | 94 | |---|---|---| 95 | | collection | `address` | The address of the newly created collection contract. | 96 | 97 | ### createNFTDropCollectionWithPaymentAddress 98 | 99 | ```solidity 100 | function createNFTDropCollectionWithPaymentAddress(string name, string symbol, string baseURI, bool isRevealed, uint32 maxTokenId, address approvedMinter, uint96 nonce, address payable paymentAddress) external nonpayable returns (address collection) 101 | ``` 102 | 103 | Create a new drop collection contract with a custom payment address. 104 | 105 | *All params other than `paymentAddress` are the same as in `createNFTDropCollection`. The nonce must be unique for the msg.sender + implementation version, otherwise this call will revert.* 106 | 107 | **Parameters** 108 | 109 | | Name | Type | Description | 110 | |---|---|---| 111 | | name | `string` | The collection's `name`. | 112 | | symbol | `string` | The collection's `symbol`. | 113 | | baseURI | `string` | The base URI for the collection. | 114 | | isRevealed | `bool` | Whether the collection is revealed or not. | 115 | | maxTokenId | `uint32` | The max token id for this collection. | 116 | | approvedMinter | `address` | An optional address to grant the MINTER_ROLE. | 117 | | nonce | `uint96` | An arbitrary value used to allow a creator to mint multiple collections with a counterfactual address. | 118 | | paymentAddress | `address payable` | The address that will receive royalties and mint payments. | 119 | 120 | **Returns** 121 | 122 | | Name | Type | Description | 123 | |---|---|---| 124 | | collection | `address` | The address of the newly created collection contract. | 125 | 126 | ### createNFTDropCollectionWithPaymentFactory 127 | 128 | ```solidity 129 | function createNFTDropCollectionWithPaymentFactory(string name, string symbol, string baseURI, bool isRevealed, uint32 maxTokenId, address approvedMinter, uint96 nonce, CallWithoutValue paymentAddressFactoryCall) external nonpayable returns (address collection) 130 | ``` 131 | 132 | 133 | 134 | 135 | 136 | **Parameters** 137 | 138 | | Name | Type | Description | 139 | |---|---|---| 140 | | name | `string` | | 141 | | symbol | `string` | | 142 | | baseURI | `string` | | 143 | | isRevealed | `bool` | | 144 | | maxTokenId | `uint32` | | 145 | | approvedMinter | `address` | | 146 | | nonce | `uint96` | | 147 | | paymentAddressFactoryCall | `CallWithoutValue` | | 148 | 149 | **Returns** 150 | 151 | | Name | Type | Description | 152 | |---|---|---| 153 | | collection | `address` | | 154 | 155 | ### implementationNFTCollection 156 | 157 | ```solidity 158 | function implementationNFTCollection() external view returns (address) 159 | ``` 160 | 161 | The address of the implementation all new NFTCollections will leverage. 162 | 163 | *When this is changed, `versionNFTCollection` is incremented.* 164 | 165 | 166 | **Returns** 167 | 168 | | Name | Type | Description | 169 | |---|---|---| 170 | | _0 | `address` | The implementation address for NFTCollection. | 171 | 172 | ### implementationNFTDropCollection 173 | 174 | ```solidity 175 | function implementationNFTDropCollection() external view returns (address) 176 | ``` 177 | 178 | The address of the implementation all new NFTDropCollections will leverage. 179 | 180 | *When this is changed, `versionNFTDropCollection` is incremented.* 181 | 182 | 183 | **Returns** 184 | 185 | | Name | Type | Description | 186 | |---|---|---| 187 | | _0 | `address` | The implementation address for NFTDropCollection. | 188 | 189 | ### initialize 190 | 191 | ```solidity 192 | function initialize(uint32 _versionNFTCollection) external nonpayable 193 | ``` 194 | 195 | Initializer called after contract creation. 196 | 197 | *This is used so that this factory will resume versions from where our original factory had left off.* 198 | 199 | **Parameters** 200 | 201 | | Name | Type | Description | 202 | |---|---|---| 203 | | _versionNFTCollection | `uint32` | The current implementation version for NFTCollections. | 204 | 205 | ### predictNFTCollectionAddress 206 | 207 | ```solidity 208 | function predictNFTCollectionAddress(address creator, uint96 nonce) external view returns (address collection) 209 | ``` 210 | 211 | Returns the address of a collection given the current implementation version, creator, and nonce. This will return the same address whether the collection has already been created or not. 212 | 213 | 214 | 215 | **Parameters** 216 | 217 | | Name | Type | Description | 218 | |---|---|---| 219 | | creator | `address` | The creator of the collection. | 220 | | nonce | `uint96` | An arbitrary value used to allow a creator to mint multiple collections with a counterfactual address. | 221 | 222 | **Returns** 223 | 224 | | Name | Type | Description | 225 | |---|---|---| 226 | | collection | `address` | The address of the collection contract that would be created by this nonce. | 227 | 228 | ### predictNFTDropCollectionAddress 229 | 230 | ```solidity 231 | function predictNFTDropCollectionAddress(address creator, uint96 nonce) external view returns (address collection) 232 | ``` 233 | 234 | Returns the address of an NFTDropCollection collection given the current implementation version, creator, and nonce. This will return the same address whether the collection has already been created or not. 235 | 236 | 237 | 238 | **Parameters** 239 | 240 | | Name | Type | Description | 241 | |---|---|---| 242 | | creator | `address` | The creator of the collection. | 243 | | nonce | `uint96` | An arbitrary value used to allow a creator to mint multiple collections with a counterfactual address. | 244 | 245 | **Returns** 246 | 247 | | Name | Type | Description | 248 | |---|---|---| 249 | | collection | `address` | The address of the collection contract that would be created by this nonce. | 250 | 251 | ### rolesManager 252 | 253 | ```solidity 254 | function rolesManager() external view returns (contract IRoles) 255 | ``` 256 | 257 | The contract address which manages common roles. 258 | 259 | *Defines a centralized admin role definition for permissioned functions below.* 260 | 261 | 262 | **Returns** 263 | 264 | | Name | Type | Description | 265 | |---|---|---| 266 | | _0 | `contract IRoles` | The contract address with role definitions. | 267 | 268 | ### versionNFTCollection 269 | 270 | ```solidity 271 | function versionNFTCollection() external view returns (uint32) 272 | ``` 273 | 274 | The implementation version of new NFTCollections. 275 | 276 | *This is auto-incremented each time `implementationNFTCollection` is changed.* 277 | 278 | 279 | **Returns** 280 | 281 | | Name | Type | Description | 282 | |---|---|---| 283 | | _0 | `uint32` | The current NFTCollection implementation version. | 284 | 285 | ### versionNFTDropCollection 286 | 287 | ```solidity 288 | function versionNFTDropCollection() external view returns (uint32) 289 | ``` 290 | 291 | The implementation version of new NFTDropCollections. 292 | 293 | *This is auto-incremented each time `implementationNFTDropCollection` is changed.* 294 | 295 | 296 | **Returns** 297 | 298 | | Name | Type | Description | 299 | |---|---|---| 300 | | _0 | `uint32` | The current NFTDropCollection implementation version. | 301 | 302 | 303 | 304 | ## Events 305 | 306 | ### ImplementationNFTCollectionUpdated 307 | 308 | ```solidity 309 | event ImplementationNFTCollectionUpdated(address indexed implementation, uint256 indexed version) 310 | ``` 311 | 312 | Emitted when the implementation of NFTCollection used by new collections is updated. 313 | 314 | 315 | 316 | **Parameters** 317 | 318 | | Name | Type | Description | 319 | |---|---|---| 320 | | implementation `indexed` | `address` | The new implementation contract address. | 321 | | version `indexed` | `uint256` | The version of the new implementation, auto-incremented. | 322 | 323 | ### ImplementationNFTDropCollectionUpdated 324 | 325 | ```solidity 326 | event ImplementationNFTDropCollectionUpdated(address indexed implementationNFTDropCollection, uint256 indexed version) 327 | ``` 328 | 329 | Emitted when the implementation of NFTDropCollection used by new collections is updated. 330 | 331 | 332 | 333 | **Parameters** 334 | 335 | | Name | Type | Description | 336 | |---|---|---| 337 | | implementationNFTDropCollection `indexed` | `address` | The new implementation contract address. | 338 | | version `indexed` | `uint256` | The version of the new implementation, auto-incremented. | 339 | 340 | ### Initialized 341 | 342 | ```solidity 343 | event Initialized(uint8 version) 344 | ``` 345 | 346 | 347 | 348 | 349 | 350 | **Parameters** 351 | 352 | | Name | Type | Description | 353 | |---|---|---| 354 | | version | `uint8` | | 355 | 356 | ### NFTCollectionCreated 357 | 358 | ```solidity 359 | event NFTCollectionCreated(address indexed collection, address indexed creator, uint256 indexed version, string name, string symbol, uint256 nonce) 360 | ``` 361 | 362 | Emitted when a new NFTCollection is created from this factory. 363 | 364 | 365 | 366 | **Parameters** 367 | 368 | | Name | Type | Description | 369 | |---|---|---| 370 | | collection `indexed` | `address` | The address of the new NFT collection contract. | 371 | | creator `indexed` | `address` | The address of the creator which owns the new collection. | 372 | | version `indexed` | `uint256` | The implementation version used by the new collection. | 373 | | name | `string` | The name of the collection contract created. | 374 | | symbol | `string` | The symbol of the collection contract created. | 375 | | nonce | `uint256` | The nonce used by the creator when creating the collection, used to define the address of the collection. | 376 | 377 | ### NFTDropCollectionCreated 378 | 379 | ```solidity 380 | event NFTDropCollectionCreated(address indexed collection, address indexed creator, address indexed approvedMinter, string name, string symbol, string baseURI, bool isRevealed, uint256 maxTokenId, address paymentAddress, uint256 version, uint256 nonce) 381 | ``` 382 | 383 | Emitted when a new NFTDropCollection is created from this factory. 384 | 385 | 386 | 387 | **Parameters** 388 | 389 | | Name | Type | Description | 390 | |---|---|---| 391 | | collection `indexed` | `address` | The address of the new NFT drop collection contract. | 392 | | creator `indexed` | `address` | The address of the creator which owns the new collection. | 393 | | approvedMinter `indexed` | `address` | An optional address to grant the MINTER_ROLE. | 394 | | name | `string` | The collection's `name`. | 395 | | symbol | `string` | The collection's `symbol`. | 396 | | baseURI | `string` | The base URI for the collection. | 397 | | isRevealed | `bool` | Whether the collection is revealed or not. | 398 | | maxTokenId | `uint256` | The max `tokenID` for this collection. | 399 | | paymentAddress | `address` | The address that will receive royalties and mint payments. | 400 | | version | `uint256` | The implementation version used by the new NFTDropCollection collection. | 401 | | nonce | `uint256` | The nonce used by the creator to create this collection. | 402 | 403 | 404 | 405 | -------------------------------------------------------------------------------- /docs/PercentSplitETH.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: PercentSplitETH 3 | description: Auto-forward ETH to a pre-determined list of addresses. 4 | --- 5 | 6 | 7 | Deploys contracts which auto-forwards any ETH sent to it to a list of recipients considering their percent share of the payment received. ERC-20 tokens are also supported and may be split on demand by calling `splitERC20Tokens`. If another asset type is sent to this contract address such as an NFT, arbitrary calls may be made by one of the split recipients in order to recover them. 8 | 9 | *Uses create2 counterfactual addresses so that the destination is known from the terms of the split.* 10 | 11 | ## Methods 12 | 13 | ### createSplit 14 | 15 | ```solidity 16 | function createSplit(PercentSplitETH.Share[] shares) external nonpayable returns (contract PercentSplitETH splitInstance) 17 | ``` 18 | 19 | 20 | 21 | 22 | 23 | **Parameters** 24 | 25 | | Name | Type | Description | 26 | |---|---|---| 27 | | shares | `PercentSplitETH.Share[]` | | 28 | 29 | **Returns** 30 | 31 | | Name | Type | Description | 32 | |---|---|---| 33 | | splitInstance | `contract PercentSplitETH` | | 34 | 35 | ### getPercentInBasisPointsByIndex 36 | 37 | ```solidity 38 | function getPercentInBasisPointsByIndex(uint256 index) external view returns (uint256 percentInBasisPoints) 39 | ``` 40 | 41 | Returns a recipient's percent share in basis points. 42 | 43 | 44 | 45 | **Parameters** 46 | 47 | | Name | Type | Description | 48 | |---|---|---| 49 | | index | `uint256` | The index of the recipient to get the share of. | 50 | 51 | **Returns** 52 | 53 | | Name | Type | Description | 54 | |---|---|---| 55 | | percentInBasisPoints | `uint256` | The percent of the payment received by the recipient, in basis points. | 56 | 57 | ### getPredictedSplitAddress 58 | 59 | ```solidity 60 | function getPredictedSplitAddress(PercentSplitETH.Share[] shares) external view returns (address splitInstance) 61 | ``` 62 | 63 | 64 | 65 | 66 | 67 | **Parameters** 68 | 69 | | Name | Type | Description | 70 | |---|---|---| 71 | | shares | `PercentSplitETH.Share[]` | | 72 | 73 | **Returns** 74 | 75 | | Name | Type | Description | 76 | |---|---|---| 77 | | splitInstance | `address` | | 78 | 79 | ### getShareLength 80 | 81 | ```solidity 82 | function getShareLength() external view returns (uint256 length) 83 | ``` 84 | 85 | Returns how many recipients are part of this split. 86 | 87 | 88 | 89 | 90 | **Returns** 91 | 92 | | Name | Type | Description | 93 | |---|---|---| 94 | | length | `uint256` | The number of recipients in this split. | 95 | 96 | ### getShareRecipientByIndex 97 | 98 | ```solidity 99 | function getShareRecipientByIndex(uint256 index) external view returns (address payable recipient) 100 | ``` 101 | 102 | Returns a recipient in this split. 103 | 104 | 105 | 106 | **Parameters** 107 | 108 | | Name | Type | Description | 109 | |---|---|---| 110 | | index | `uint256` | The index of the recipient to get. | 111 | 112 | **Returns** 113 | 114 | | Name | Type | Description | 115 | |---|---|---| 116 | | recipient | `address payable` | The recipient at the given index. | 117 | 118 | ### getShares 119 | 120 | ```solidity 121 | function getShares() external view returns (struct PercentSplitETH.Share[] shares) 122 | ``` 123 | 124 | Returns a tuple with the terms of this split. 125 | 126 | 127 | 128 | 129 | **Returns** 130 | 131 | | Name | Type | Description | 132 | |---|---|---| 133 | | shares | `PercentSplitETH.Share[]` | The list of recipients and their share of the payment for this split. | 134 | 135 | ### initialize 136 | 137 | ```solidity 138 | function initialize(PercentSplitETH.Share[] shares) external nonpayable 139 | ``` 140 | 141 | 142 | 143 | 144 | 145 | **Parameters** 146 | 147 | | Name | Type | Description | 148 | |---|---|---| 149 | | shares | `PercentSplitETH.Share[]` | | 150 | 151 | ### proxyCall 152 | 153 | ```solidity 154 | function proxyCall(address payable target, bytes callData) external nonpayable 155 | ``` 156 | 157 | Allows the split recipients to make an arbitrary contract call. 158 | 159 | *This is provided to allow recovering from unexpected scenarios, such as receiving an NFT at this address. It will first attempt a fair split of ERC20 tokens before proceeding. This contract is built to split ETH payments. The ability to attempt to make other calls is here just in case other assets were also sent so that they don't get locked forever in the contract.* 160 | 161 | **Parameters** 162 | 163 | | Name | Type | Description | 164 | |---|---|---| 165 | | target | `address payable` | The address of the contract to call. | 166 | | callData | `bytes` | The data to send to the `target` contract. | 167 | 168 | ### splitERC20Tokens 169 | 170 | ```solidity 171 | function splitERC20Tokens(contract IERC20 erc20Contract) external nonpayable 172 | ``` 173 | 174 | Anyone can call this function to split all available tokens at the provided address between the recipients. 175 | 176 | *This contract is built to split ETH payments. The ability to attempt to split ERC20 tokens is here just in case tokens were also sent so that they don't get locked forever in the contract.* 177 | 178 | **Parameters** 179 | 180 | | Name | Type | Description | 181 | |---|---|---| 182 | | erc20Contract | `contract IERC20` | The address of the ERC20 token contract to split tokens for. | 183 | 184 | ### splitETH 185 | 186 | ```solidity 187 | function splitETH() external nonpayable 188 | ``` 189 | 190 | Allows any ETH stored by the contract to be split among recipients. 191 | 192 | *Normally ETH is forwarded as it comes in, but a balance in this contract is possible if it was sent before the contract was created or if self destruct was used.* 193 | 194 | 195 | 196 | 197 | ## Events 198 | 199 | ### ERC20Transferred 200 | 201 | ```solidity 202 | event ERC20Transferred(address indexed erc20Contract, address indexed account, uint256 amount) 203 | ``` 204 | 205 | Emitted when an ERC20 token is transferred to a recipient through this split contract. 206 | 207 | 208 | 209 | **Parameters** 210 | 211 | | Name | Type | Description | 212 | |---|---|---| 213 | | erc20Contract `indexed` | `address` | The address of the ERC20 token contract. | 214 | | account `indexed` | `address` | The account which received payment. | 215 | | amount | `uint256` | The amount of ERC20 tokens sent to this recipient. | 216 | 217 | ### ETHTransferred 218 | 219 | ```solidity 220 | event ETHTransferred(address indexed account, uint256 amount) 221 | ``` 222 | 223 | Emitted when ETH is transferred to a recipient through this split contract. 224 | 225 | 226 | 227 | **Parameters** 228 | 229 | | Name | Type | Description | 230 | |---|---|---| 231 | | account `indexed` | `address` | The account which received payment. | 232 | | amount | `uint256` | The amount of ETH payment sent to this recipient. | 233 | 234 | ### Initialized 235 | 236 | ```solidity 237 | event Initialized(uint8 version) 238 | ``` 239 | 240 | 241 | 242 | 243 | 244 | **Parameters** 245 | 246 | | Name | Type | Description | 247 | |---|---|---| 248 | | version | `uint8` | | 249 | 250 | ### PercentSplitCreated 251 | 252 | ```solidity 253 | event PercentSplitCreated(address indexed contractAddress) 254 | ``` 255 | 256 | Emitted when a new percent split contract is created from this factory. 257 | 258 | 259 | 260 | **Parameters** 261 | 262 | | Name | Type | Description | 263 | |---|---|---| 264 | | contractAddress `indexed` | `address` | The address of the new percent split contract. | 265 | 266 | ### PercentSplitShare 267 | 268 | ```solidity 269 | event PercentSplitShare(address indexed recipient, uint256 percentInBasisPoints) 270 | ``` 271 | 272 | Emitted for each share of the split being defined. 273 | 274 | 275 | 276 | **Parameters** 277 | 278 | | Name | Type | Description | 279 | |---|---|---| 280 | | recipient `indexed` | `address` | The address of the recipient when payment to the split is received. | 281 | | percentInBasisPoints | `uint256` | The percent of the payment received by the recipient, in basis points. | 282 | 283 | 284 | 285 | -------------------------------------------------------------------------------- /docs_template.sqrl: -------------------------------------------------------------------------------- 1 | --- 2 | {{@if (it.name)}}title: {{it.name}}{{/if}} 3 | 4 | {{@if (it.title)}}description: {{it.title}}{{/if}} 5 | 6 | --- 7 | 8 | 9 | {{@if (it.notice)}}{{it.notice}}{{/if}} 10 | 11 | 12 | {{@if (it.details)}}*{{it.details}}*{{/if}} 13 | 14 | 15 | {{@if (Object.keys(it.methods).length > 0)}} 16 | ## Methods 17 | 18 | {{@foreach(it.methods) => key, val}} 19 | ### {{key.split('(')[0]}} 20 | 21 | 22 | ```solidity 23 | {{val.code}} 24 | 25 | ``` 26 | 27 | {{@if (val.notice)}}{{val.notice}}{{/if}} 28 | 29 | 30 | {{@if (val.details)}}*{{val.details}}*{{/if}} 31 | 32 | 33 | {{@if (Object.keys(val.inputs).length > 0)}} 34 | **Parameters** 35 | 36 | | Name | Type | Description | 37 | |---|---|---| 38 | {{@foreach(val.inputs) => key, val}} 39 | | {{key}} | `{{val.type}}` | {{@if (val.description)}}{{val.description}}{{/if}} | 40 | {{/foreach}} 41 | {{/if}} 42 | 43 | {{@if (Object.keys(val.outputs).length > 0)}} 44 | **Returns** 45 | 46 | | Name | Type | Description | 47 | |---|---|---| 48 | {{@foreach(val.outputs) => key, val}} 49 | | {{key}} | `{{val.type}}` | {{@if (val.description)}}{{val.description}}{{/if}} | 50 | {{/foreach}} 51 | 52 | {{/if}} 53 | {{/foreach}} 54 | 55 | {{/if}} 56 | 57 | {{@if (Object.keys(it.events).length > 0)}} 58 | ## Events 59 | 60 | {{@foreach(it.events) => key, val}} 61 | ### {{key}} 62 | 63 | 64 | ```solidity 65 | {{val.code}} 66 | 67 | ``` 68 | 69 | {{@if (val.notice)}}{{val.notice}}{{/if}} 70 | 71 | 72 | {{@if (val.details)}}*{{val.details}}*{{/if}} 73 | 74 | 75 | {{@if (Object.keys(val.inputs).length > 0)}} 76 | **Parameters** 77 | 78 | | Name | Type | Description | 79 | |---|---|---| 80 | {{@foreach(val.inputs) => key, val}} 81 | | {{key}} {{@if (val.indexed)}}`indexed`{{/if}} | `{{val.type}}` | {{@if (val.description)}}{{val.description}}{{/if}} | 82 | {{/foreach}} 83 | {{/if}} 84 | 85 | {{/foreach}} 86 | 87 | {{/if}} 88 | 89 | {{@if (Object.keys(it.errors).length > 0)}} 90 | ## Errors 91 | 92 | {{@foreach(it.errors) => key, val}} 93 | ### {{key}} 94 | 95 | 96 | ```solidity 97 | {{val.code}} 98 | 99 | ``` 100 | 101 | {{@if (val.notice)}}{{val.notice}}{{/if}} 102 | 103 | 104 | {{@if (val.details)}}*{{val.details}}*{{/if}} 105 | 106 | 107 | {{@if (Object.keys(val.inputs).length > 0)}} 108 | **Parameters** 109 | 110 | | Name | Type | Description | 111 | |---|---|---| 112 | {{@foreach(val.inputs) => key, val}} 113 | | {{key}} | `{{val.type}}` | {{@if (val.description)}}{{val.description}}{{/if}} | 114 | {{/foreach}} 115 | {{/if}} 116 | 117 | {{/foreach}} 118 | 119 | {{/if}} -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "hardhat-exposed"; 2 | import "@nomicfoundation/hardhat-chai-matchers"; 3 | import "@typechain/hardhat"; 4 | import "solidity-coverage"; 5 | import "hardhat-deploy"; 6 | import "hardhat-deploy-ethers"; 7 | import "@openzeppelin/hardhat-upgrades"; 8 | import "@nomiclabs/hardhat-etherscan"; 9 | import "hardhat-preprocessor"; 10 | import "hardhat-tracer"; 11 | import "hardhat-storage-layout"; 12 | import "hardhat-exposed"; 13 | import "hardhat-gas-reporter"; 14 | import "@primitivefi/hardhat-dodoc"; 15 | 16 | import { HardhatUserConfig } from "hardhat/types"; 17 | 18 | const config: HardhatUserConfig = { 19 | solidity: { 20 | version: "0.8.17", 21 | settings: { 22 | optimizer: { 23 | enabled: true, 24 | runs: 1337, 25 | }, 26 | }, 27 | }, 28 | networks: { 29 | hardhat: { 30 | accounts: { 31 | count: 100, 32 | }, 33 | }, 34 | }, 35 | typechain: { 36 | target: "ethers-v5", 37 | externalArtifacts: ["node_modules/@manifoldxyz/royalty-registry-solidity/build/contracts/*.json"], 38 | outDir: "src/typechain", 39 | }, 40 | gasReporter: { 41 | excludeContracts: ["mocks/", "FoundationTreasury.sol", "ERC721.sol"], 42 | }, 43 | dodoc: { 44 | runOnCompile: true, 45 | include: [ 46 | "NFTCollection", 47 | "NFTDropCollection", 48 | "FETH", 49 | "FNDMiddleware", 50 | "NFTCollectionFactory", 51 | "NFTMarket", 52 | "NFTDropMarket", 53 | "FoundationTreasury", 54 | "PercentSplitETH", 55 | ], 56 | exclude: ["mixins", "mocks", "interfaces", "xyz/royalty-registry-solidity", "archive", "contracts-exposed"], 57 | templatePath: "./docs_template.sqrl", 58 | }, 59 | }; 60 | 61 | export default config; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@f8n/fnd-protocol", 3 | "description": "Foundation smart contracts", 4 | "version": "2.3.0", 5 | "contributors": [ 6 | { 7 | "name": "batu-inal", 8 | "url": "https://twitter.com/Ba2Inal" 9 | }, 10 | { 11 | "name": "HardlyDifficult", 12 | "url": "https://twitter.com/HardlyDifficult" 13 | } 14 | ], 15 | "scripts": { 16 | "build": "hardhat typechain", 17 | "clean": "hardhat clean", 18 | "lint": "yarn prettier && yarn lint:sol && yarn lint:ts", 19 | "lint:sol": "solhint --config ./.solhint.json --fix \"contracts/**/*.sol\"", 20 | "lint:ts": "eslint --config ./.eslintrc.yaml --fix --ext .js,.ts .", 21 | "prettier": "prettier --config .prettierrc --write --list-different \"**/*.{js,json,md,sol,ts}\"" 22 | }, 23 | "devDependencies": { 24 | "@ensdomains/eth-ens-namehash": "2.0.15", 25 | "@ethersproject/abstract-signer": "5.7.0", 26 | "@ethersproject/bignumber": "5.7.0", 27 | "@manifoldxyz/royalty-registry-solidity": "1.0.9", 28 | "@nomicfoundation/hardhat-chai-matchers": "1.0.3", 29 | "@nomicfoundation/hardhat-toolbox": "1.0.2", 30 | "@nomiclabs/hardhat-ethers": "2.1.1", 31 | "@nomiclabs/hardhat-etherscan": "3.1.0", 32 | "@openzeppelin/contracts": "4.7.3", 33 | "@openzeppelin/contracts-upgradeable": "4.7.3", 34 | "@openzeppelin/hardhat-upgrades": "1.20.0", 35 | "@openzeppelin/test-helpers": "0.5.16", 36 | "@primitivefi/hardhat-dodoc": "0.2.3", 37 | "@typechain/ethers-v5": "9.0.0", 38 | "@typechain/hardhat": "6.1.3", 39 | "@types/chai": "4.3.3", 40 | "@types/fs-extra": "9.0.13", 41 | "@types/mocha": "9.1.1", 42 | "@types/node": "18.7.18", 43 | "@typescript-eslint/eslint-plugin": "5.37.0", 44 | "@typescript-eslint/parser": "5.37.0", 45 | "chai": "4.3.6", 46 | "chalk": "4.1.2", 47 | "coveralls": "3.1.1", 48 | "defender-admin-client": "1.31.1", 49 | "dotenv": "16.0.2", 50 | "eslint": "8.23.1", 51 | "eslint-config-prettier": "8.5.0", 52 | "eslint-plugin-no-only-tests": "3.0.0", 53 | "eslint-plugin-simple-import-sort": "8.0.0", 54 | "eslint-plugin-unused-imports": "2.0.0", 55 | "ethereum-waffle": "3.4.4", 56 | "ethereumjs-util": "7.1.5", 57 | "ethers": "5.7.1", 58 | "fs-extra": "10.1.0", 59 | "hardhat": "2.11.1", 60 | "hardhat-contract-sizer": "2.6.1", 61 | "hardhat-deploy": "0.11.15", 62 | "hardhat-deploy-ethers": "0.3.0-beta.13", 63 | "hardhat-exposed": "0.2.8", 64 | "hardhat-gas-reporter": "1.0.9", 65 | "hardhat-helpers": "0.1.28", 66 | "hardhat-output-validator": "0.1.19", 67 | "hardhat-preprocessor": "0.1.5", 68 | "hardhat-storage-layout": "0.1.6", 69 | "hardhat-tracer": "1.1.0-rc.9", 70 | "mocha": "10.0.0", 71 | "npm-package-json-lint": "6.3.0", 72 | "npm-package-json-lint-config-default": "5.0.0", 73 | "prettier": "2.7.1", 74 | "prettier-plugin-solidity": "1.0.0-dev.23", 75 | "shelljs": "0.8.5", 76 | "solhint": "3.3.7", 77 | "solhint-plugin-prettier": "0.0.5", 78 | "solidity-coverage": "0.8.2", 79 | "ts-node": "10.9.1", 80 | "typechain": "8.1.0", 81 | "typescript": "4.8.3" 82 | }, 83 | "files": [ 84 | "addresses.js", 85 | "subgraphEndpoints.js", 86 | "artifacts", 87 | "contracts" 88 | ], 89 | "license": "MIT", 90 | "repository": { 91 | "type": "git", 92 | "url": "https://github.com/f8n/fnd-protocol" 93 | }, 94 | "bugs": { 95 | "url": "https://github.com/f8n/fnd-protocol/issues" 96 | }, 97 | "homepage": "https://github.com/f8n/fnd-protocol" 98 | } 99 | -------------------------------------------------------------------------------- /subgraph/codegen.yml: -------------------------------------------------------------------------------- 1 | overwrite: true 2 | schema: 3 | - "schema.graphql" 4 | - "scalars.graphql" 5 | generates: 6 | ../src/types/generated/subgraph.tsx: 7 | plugins: 8 | - "typescript" 9 | -------------------------------------------------------------------------------- /subgraph/scalars.graphql: -------------------------------------------------------------------------------- 1 | scalar BigInt 2 | scalar BigDecimal 3 | scalar Bytes -------------------------------------------------------------------------------- /subgraph/src/mappings/collections.ts: -------------------------------------------------------------------------------- 1 | import { Address, BigInt } from "@graphprotocol/graph-ts"; 2 | 3 | import { CollectionCreated as CollectionCreatedEvent } from "../../generated/FNDCollectionFactory/FNDCollectionFactoryContract"; 4 | import { 5 | NFTCollectionCreated as CollectionCreatedEventV2, 6 | NFTDropCollectionCreated as NFTDropCollectionCreatedEvent, 7 | } from "../../generated/NFTCollectionFactory/NFTCollectionFactoryContract"; 8 | import { CollectionContract, NftDropCollectionContract } from "../../generated/schema"; 9 | import { 10 | NFTCollection as NFTCollectionTemplate, 11 | NFTDropCollection as NFTDropCollectionTemplate, 12 | } from "../../generated/templates"; 13 | import { loadOrCreateAccount } from "../shared/accounts"; 14 | import { ZERO_ADDRESS_STRING } from "../shared/constants"; 15 | import { loadOrCreateCreator } from "../shared/creators"; 16 | import { loadOrCreateNFTContract } from "./nft"; 17 | 18 | export function handleCollectionCreated(event: CollectionCreatedEvent): void { 19 | _handleCollectionCreated( 20 | event.params.collection, 21 | event.params.creator, 22 | event.params.version, 23 | event.address, 24 | event.block.timestamp, 25 | ); 26 | } 27 | 28 | export function handleCollectionCreatedV2(event: CollectionCreatedEventV2): void { 29 | _handleCollectionCreated( 30 | event.params.collection, 31 | event.params.creator, 32 | event.params.version, 33 | event.address, 34 | event.block.timestamp, 35 | ); 36 | } 37 | 38 | function _handleCollectionCreated( 39 | nftCollection: Address, 40 | creator: Address, 41 | version: BigInt, 42 | eventAddress: Address, 43 | eventTS: BigInt, 44 | ): void { 45 | NFTCollectionTemplate.create(nftCollection); 46 | let nftContract = loadOrCreateNFTContract(nftCollection); 47 | let collectionEntity = CollectionContract.load(nftCollection.toHex()); 48 | if (collectionEntity) { 49 | collectionEntity.dateSelfDestructed = null; 50 | } else { 51 | collectionEntity = new CollectionContract(nftCollection.toHex()); 52 | } 53 | collectionEntity.nftContract = nftContract.id; 54 | collectionEntity.creator = loadOrCreateCreator(creator).id; 55 | collectionEntity.version = eventAddress.toHex() + "-" + version.toString(); 56 | collectionEntity.dateCreated = eventTS; 57 | collectionEntity.save(); 58 | } 59 | 60 | export function handleNFTDropCollectionCreated(event: NFTDropCollectionCreatedEvent): void { 61 | NFTDropCollectionTemplate.create(event.params.collection); 62 | let nftDropCollection = NftDropCollectionContract.load(event.params.collection.toHex()); 63 | if (nftDropCollection) { 64 | nftDropCollection.dateSelfDestructed = null; 65 | } else { 66 | nftDropCollection = new NftDropCollectionContract(event.params.collection.toHex()); 67 | } 68 | let nftContract = loadOrCreateNFTContract(event.params.collection, /*fromNFTDropCollection=*/ true); 69 | nftDropCollection.nftContract = nftContract.id; 70 | nftDropCollection.creator = loadOrCreateCreator(event.params.creator).id; 71 | nftDropCollection.dateCreated = event.block.timestamp; 72 | if (event.params.approvedMinter.toHex() != ZERO_ADDRESS_STRING) { 73 | nftDropCollection.approvedMinter = loadOrCreateAccount(event.params.approvedMinter).id; 74 | } 75 | nftDropCollection.paymentAddress = loadOrCreateAccount(event.params.paymentAddress).id; 76 | nftDropCollection.version = event.address.toHex() + "-" + event.params.version.toString(); 77 | nftDropCollection.save(); 78 | } 79 | -------------------------------------------------------------------------------- /subgraph/src/mappings/feth.ts: -------------------------------------------------------------------------------- 1 | import { BalanceLocked, BalanceUnlocked, ETHWithdrawn, Transfer } from "../../generated/Feth/FethContract"; 2 | import { loadOrCreateAccount } from "../shared/accounts"; 3 | import { ZERO_ADDRESS_STRING, ZERO_BIG_DECIMAL } from "../shared/constants"; 4 | import { toETH } from "../shared/conversions"; 5 | import { loadOrCreateFeth, loadOrCreateFethEscrow } from "../shared/feth"; 6 | 7 | export function handleTransfer(event: Transfer): void { 8 | if (event.params.from.toHex() != ZERO_ADDRESS_STRING) { 9 | let from = loadOrCreateAccount(event.params.from); 10 | let fethFrom = loadOrCreateFeth(from, event.block); 11 | fethFrom.balanceInETH = fethFrom.balanceInETH.minus(toETH(event.params.amount)); 12 | fethFrom.dateLastUpdated = event.block.timestamp; 13 | fethFrom.save(); 14 | } 15 | 16 | let to = loadOrCreateAccount(event.params.to); 17 | let fethTo = loadOrCreateFeth(to, event.block); 18 | fethTo.balanceInETH = fethTo.balanceInETH.plus(toETH(event.params.amount)); 19 | fethTo.dateLastUpdated = event.block.timestamp; 20 | fethTo.save(); 21 | } 22 | 23 | export function handleETHWithdrawn(event: ETHWithdrawn): void { 24 | let from = loadOrCreateAccount(event.params.from); 25 | let fethFrom = loadOrCreateFeth(from, event.block); 26 | fethFrom.balanceInETH = fethFrom.balanceInETH.minus(toETH(event.params.amount)); 27 | fethFrom.dateLastUpdated = event.block.timestamp; 28 | fethFrom.save(); 29 | } 30 | 31 | export function handleBalanceLocked(event: BalanceLocked): void { 32 | let to = loadOrCreateAccount(event.params.account); 33 | let fethTo = loadOrCreateFeth(to, event.block); 34 | fethTo.balanceInETH = fethTo.balanceInETH.plus(toETH(event.params.valueDeposited)); 35 | fethTo.dateLastUpdated = event.block.timestamp; 36 | fethTo.save(); 37 | let escrow = loadOrCreateFethEscrow(event, to); 38 | if (escrow.dateRemoved) { 39 | escrow.amountInETH = toETH(event.params.amount); 40 | escrow.dateRemoved = null; 41 | escrow.transactionHashRemoved = null; 42 | } else { 43 | escrow.amountInETH = escrow.amountInETH.plus(toETH(event.params.amount)); 44 | } 45 | 46 | escrow.dateExpiry = event.params.expiration; 47 | escrow.transactionHashCreated = event.transaction.hash; 48 | escrow.save(); 49 | } 50 | 51 | export function handleBalanceUnlocked(event: BalanceUnlocked): void { 52 | let from = loadOrCreateAccount(event.params.account); 53 | let escrow = loadOrCreateFethEscrow(event, from); 54 | escrow.amountInETH = escrow.amountInETH.minus(toETH(event.params.amount)); 55 | if (escrow.amountInETH.equals(ZERO_BIG_DECIMAL)) { 56 | escrow.transactionHashRemoved = event.transaction.hash; 57 | escrow.dateRemoved = event.block.timestamp; 58 | } 59 | escrow.save(); 60 | } 61 | -------------------------------------------------------------------------------- /subgraph/src/mappings/nft.ts: -------------------------------------------------------------------------------- 1 | import { Address, BigInt, ethereum, store } from "@graphprotocol/graph-ts"; 2 | 3 | import { 4 | NFTOwnerMigrated, 5 | PaymentAddressMigrated, 6 | TokenCreatorPaymentAddressSet, 7 | TokenCreatorUpdated, 8 | } from "../../generated/NFT721Contract/NFT721Contract"; 9 | import { 10 | CollectionContract, 11 | FixedPriceSaleMint, 12 | Nft, 13 | NftAccountApproval, 14 | NftContract, 15 | NftDropCollectionContract, 16 | NftTransfer, 17 | } from "../../generated/schema"; 18 | import { 19 | Approval, 20 | ApprovalForAll, 21 | BaseURIUpdated, 22 | Minted, 23 | NFTCollection as NFTCollectionABI, 24 | SelfDestruct, 25 | Transfer, 26 | } from "../../generated/templates/NFTCollection/NFTCollection"; 27 | import { 28 | MaxTokenIdUpdated, 29 | NFTDropCollection, 30 | URIUpdated, 31 | } from "../../generated/templates/NFTDropCollection/NFTDropCollection"; 32 | import { loadOrCreateAccount } from "../shared/accounts"; 33 | import { ZERO_ADDRESS_STRING, ZERO_BIG_DECIMAL } from "../shared/constants"; 34 | import { loadOrCreateCreator } from "../shared/creators"; 35 | import { recordNftEvent, removePreviousTransferEvent } from "../shared/events"; 36 | import { getLogId } from "../shared/ids"; 37 | 38 | export function loadOrCreateNFTContract(address: Address, fromNFTDropCollection: boolean = false): NftContract { 39 | let nftContract = NftContract.load(address.toHex()); 40 | if (!nftContract) { 41 | if (fromNFTDropCollection) { 42 | nftContract = _createNFTContractFromNFTDropCollection(address); 43 | } else { 44 | nftContract = _createNFTContractFromCollection(address); 45 | } 46 | } 47 | return nftContract as NftContract; 48 | } 49 | 50 | function _createNFTContractFromCollection(address: Address): NftContract { 51 | let nftContract = new NftContract(address.toHex()); 52 | let contract = NFTCollectionABI.bind(address); 53 | let nameResults = contract.try_name(); 54 | if (!nameResults.reverted) { 55 | nftContract.name = nameResults.value; 56 | } 57 | let symbolResults = contract.try_symbol(); 58 | if (!symbolResults.reverted) { 59 | nftContract.symbol = symbolResults.value; 60 | } 61 | nftContract.baseURI = "ipfs://"; 62 | nftContract.save(); 63 | return nftContract; 64 | } 65 | 66 | function _createNFTContractFromNFTDropCollection(address: Address): NftContract { 67 | let nftContract = new NftContract(address.toHex()); 68 | let contract = NFTDropCollection.bind(address); 69 | let nameResults = contract.try_name(); 70 | if (!nameResults.reverted) { 71 | nftContract.name = nameResults.value; 72 | } 73 | let symbolResults = contract.try_symbol(); 74 | if (!symbolResults.reverted) { 75 | nftContract.symbol = symbolResults.value; 76 | } 77 | let baseURIResults = contract.try_baseURI(); 78 | if (!baseURIResults.reverted) { 79 | nftContract.baseURI = baseURIResults.value; 80 | } 81 | let maxTokenIdResults = contract.try_maxTokenId(); 82 | if (!maxTokenIdResults.reverted) { 83 | nftContract.maxTokenID = maxTokenIdResults.value; 84 | } 85 | nftContract.save(); 86 | return nftContract; 87 | } 88 | 89 | function getNFTId(address: Address, id: BigInt): string { 90 | return address.toHex() + "-" + id.toString(); 91 | } 92 | 93 | export function loadOrCreateNFT(address: Address, id: BigInt, event: ethereum.Event): Nft { 94 | let nftId = getNFTId(address, id); 95 | let nft = Nft.load(nftId); 96 | if (!nft) { 97 | nft = new Nft(nftId); 98 | nft.nftContract = loadOrCreateNFTContract(address).id; 99 | nft.tokenId = id; 100 | nft.dateMinted = event.block.timestamp; 101 | let contract = NFTCollectionABI.bind(address); 102 | let ownerResult = contract.try_ownerOf(id); 103 | if (!ownerResult.reverted) { 104 | nft.owner = loadOrCreateAccount(ownerResult.value).id; 105 | } else { 106 | nft.owner = loadOrCreateAccount(Address.zero()).id; 107 | } 108 | nft.ownedOrListedBy = nft.owner; 109 | nft.netSalesInETH = ZERO_BIG_DECIMAL; 110 | nft.netSalesPendingInETH = ZERO_BIG_DECIMAL; 111 | nft.netRevenueInETH = ZERO_BIG_DECIMAL; 112 | nft.netRevenuePendingInETH = ZERO_BIG_DECIMAL; 113 | nft.isFirstSale = true; 114 | let pathResult = contract.try_tokenURI(id); 115 | if (!pathResult.reverted) { 116 | nft.tokenIPFSPath = pathResult.value; 117 | } 118 | let creatorResult = contract.try_tokenCreator(id); 119 | if (!creatorResult.reverted) { 120 | nft.creator = loadOrCreateCreator(creatorResult.value).id; 121 | } 122 | nft.save(); 123 | } 124 | 125 | return nft as Nft; 126 | } 127 | 128 | export function handleApproval(event: Approval): void { 129 | let nft = loadOrCreateNFT(event.address, event.params.tokenId, event); 130 | if (event.params.approved != Address.zero()) { 131 | nft.approvedSpender = loadOrCreateAccount(event.params.approved).id; 132 | } else { 133 | nft.approvedSpender = null; 134 | } 135 | nft.save(); 136 | } 137 | 138 | export function handleApprovalForAll(event: ApprovalForAll): void { 139 | let id = event.address.toHex() + "-" + event.params.owner.toHex() + "-" + event.params.operator.toHex(); 140 | if (event.params.approved) { 141 | let nft721AccountApproval = new NftAccountApproval(id); 142 | let nftContract = loadOrCreateNFTContract(event.address); 143 | nft721AccountApproval.nftContract = nftContract.id; 144 | nft721AccountApproval.owner = loadOrCreateAccount(event.params.owner).id; 145 | nft721AccountApproval.spender = loadOrCreateAccount(event.params.operator).id; 146 | nft721AccountApproval.save(); 147 | } else { 148 | store.remove("NftAccountApproval", id); 149 | } 150 | } 151 | 152 | export function handleBaseURIUpdated(event: BaseURIUpdated): void { 153 | _handleBaseURIUpdated(event.address, event.params.baseURI); 154 | } 155 | 156 | export function handleURIUpdated(event: URIUpdated): void { 157 | _handleBaseURIUpdated(event.address, event.params.baseURI); 158 | } 159 | 160 | function _handleBaseURIUpdated(eventAddress: Address, baseURI: string): void { 161 | let nftContract = loadOrCreateNFTContract(eventAddress); 162 | nftContract.baseURI = baseURI; 163 | nftContract.save(); 164 | } 165 | 166 | export function handleMaxTokenIdUpdated(event: MaxTokenIdUpdated): void { 167 | let nftContract = loadOrCreateNFTContract(event.address); 168 | nftContract.maxTokenID = event.params.maxTokenId; 169 | nftContract.save(); 170 | } 171 | 172 | export function handleTokenCreatorUpdated(event: TokenCreatorUpdated): void { 173 | let nft = loadOrCreateNFT(event.address, event.params.tokenId, event); 174 | nft.creator = loadOrCreateCreator(event.params.toCreator).id; 175 | nft.save(); 176 | 177 | if (event.params.fromCreator.toHex() != ZERO_ADDRESS_STRING) { 178 | recordNftEvent( 179 | event, 180 | nft as Nft, 181 | "CreatorMigrated", 182 | loadOrCreateAccount(event.params.fromCreator), 183 | null, 184 | null, 185 | null, 186 | loadOrCreateAccount(event.params.toCreator), 187 | ); 188 | } 189 | } 190 | 191 | export function handleMinted(event: Minted): void { 192 | let nft = loadOrCreateNFT(event.address, event.params.tokenId, event); 193 | updateTokenIPFSPath(nft as Nft, event.params.tokenCID.toString(), event.block.timestamp); 194 | 195 | nft.creator = loadOrCreateCreator(event.params.creator).id; 196 | nft.save(); 197 | let creatorAccount = loadOrCreateAccount(event.params.creator); 198 | recordNftEvent(event, nft as Nft, "Minted", creatorAccount, null, null, null, creatorAccount); 199 | } 200 | 201 | export function handleTransfer(event: Transfer): void { 202 | let nftContract = loadOrCreateNFTContract(event.address); 203 | let nftId = getNFTId(event.address, event.params.tokenId); 204 | let nft: Nft | null; 205 | if (event.params.from.toHex() == ZERO_ADDRESS_STRING) { 206 | // Mint 207 | nft = new Nft(nftId); 208 | nft.nftContract = nftContract.id; 209 | nft.tokenId = event.params.tokenId; 210 | nft.dateMinted = event.block.timestamp; 211 | nft.owner = loadOrCreateAccount(event.params.to).id; 212 | nft.ownedOrListedBy = nft.owner; 213 | nft.netSalesInETH = ZERO_BIG_DECIMAL; 214 | nft.netSalesPendingInETH = ZERO_BIG_DECIMAL; 215 | nft.netRevenueInETH = ZERO_BIG_DECIMAL; 216 | nft.netRevenuePendingInETH = ZERO_BIG_DECIMAL; 217 | nft.isFirstSale = true; 218 | nft.save(); 219 | // Only for NftDropCollectionContract add mint event here. 220 | // NFTCollection handles this in the handleMint handler. 221 | let nftDropCollection = NftDropCollectionContract.load(nftContract.id); 222 | if (nftDropCollection) { 223 | if (event.transaction.from.toHexString() !== nftDropCollection.creator) { 224 | if (!FixedPriceSaleMint.load(event.transaction.hash.toHex())) { 225 | let fixedPriceSaleMint = new FixedPriceSaleMint(event.transaction.hash.toHex()); 226 | fixedPriceSaleMint.fixedPriceSale = nftContract.id; 227 | fixedPriceSaleMint.save(); 228 | } 229 | } 230 | let creatorAccount = loadOrCreateAccount(Address.fromString(nftDropCollection.creator)); 231 | recordNftEvent(event, nft as Nft, "Minted", creatorAccount, null, null, null, creatorAccount); 232 | } 233 | } else { 234 | // Transfer or Burn 235 | nft = loadOrCreateNFT(event.address, event.params.tokenId, event); 236 | nft.owner = loadOrCreateAccount(event.params.to).id; 237 | nft.ownedOrListedBy = nft.owner; 238 | 239 | if (event.params.to.toHex() == ZERO_ADDRESS_STRING) { 240 | // Burn 241 | recordNftEvent(event, nft as Nft, "Burned", loadOrCreateAccount(event.params.from)); 242 | } else { 243 | // Transfer 244 | recordNftEvent( 245 | event, 246 | nft as Nft, 247 | "Transferred", 248 | loadOrCreateAccount(event.params.from), 249 | null, 250 | null, 251 | null, 252 | loadOrCreateAccount(event.params.to), 253 | ); 254 | } 255 | } 256 | 257 | let transfer = new NftTransfer(getLogId(event)); 258 | transfer.nft = nftId; 259 | transfer.from = loadOrCreateAccount(event.params.from).id; 260 | transfer.to = loadOrCreateAccount(event.params.to).id; 261 | transfer.dateTransferred = event.block.timestamp; 262 | transfer.transactionHash = event.transaction.hash; 263 | transfer.save(); 264 | 265 | if (event.params.from.toHex() == ZERO_ADDRESS_STRING) { 266 | nft.mintedTransfer = transfer.id; 267 | } 268 | nft.save(); 269 | } 270 | 271 | function updateTokenIPFSPath(nft: Nft, tokenIPFSPath: string, _: BigInt): void { 272 | nft.tokenIPFSPath = tokenIPFSPath; 273 | nft.save(); 274 | } 275 | 276 | export function handleTokenCreatorPaymentAddressSet(event: TokenCreatorPaymentAddressSet): void { 277 | let nft = loadOrCreateNFT(event.address, event.params.tokenId, event); 278 | nft.tokenCreatorPaymentAddress = event.params.toPaymentAddress; 279 | // This field resets to null if a PercentSplit does not exist at this address 280 | nft.percentSplit = event.params.toPaymentAddress.toHex(); 281 | nft.save(); 282 | } 283 | 284 | export function handlePaymentAddressMigrated(event: PaymentAddressMigrated): void { 285 | let nft = loadOrCreateNFT(event.address, event.params.tokenId, event); 286 | let originalAccount = loadOrCreateAccount(event.params.originalAddress); 287 | let newAccount = loadOrCreateAccount(event.params.newAddress); 288 | 289 | recordNftEvent(event, nft as Nft, "CreatorPaymentAddressMigrated", originalAccount, null, null, null, newAccount); 290 | } 291 | 292 | export function handleNFTOwnerMigrated(event: NFTOwnerMigrated): void { 293 | let nft = loadOrCreateNFT(event.address, event.params.tokenId, event); 294 | recordNftEvent( 295 | event, 296 | nft as Nft, 297 | "OwnerMigrated", 298 | loadOrCreateAccount(event.params.originalAddress), 299 | null, 300 | null, 301 | null, 302 | loadOrCreateAccount(event.params.newAddress), 303 | ); 304 | removePreviousTransferEvent(event); 305 | } 306 | 307 | export function handleSelfDestruct(event: SelfDestruct): void { 308 | let collection = CollectionContract.load(event.address.toHex()); 309 | if (collection) { 310 | collection.dateSelfDestructed = event.block.timestamp; 311 | collection.save(); 312 | return; 313 | } 314 | let nftDropCollection = NftDropCollectionContract.load(event.address.toHex()); 315 | if (nftDropCollection) { 316 | nftDropCollection.dateSelfDestructed = event.block.timestamp; 317 | nftDropCollection.save(); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /subgraph/src/mappings/nftDropMarket.ts: -------------------------------------------------------------------------------- 1 | import { BigDecimal, BigInt } from "@graphprotocol/graph-ts"; 2 | 3 | import { 4 | BuyReferralPaid, 5 | CreateFixedPriceSale, 6 | MintFromFixedPriceDrop, 7 | } from "../../generated/NFTDropMarket/NFTDropMarketContract"; 8 | import { FixedPriceSale, FixedPriceSaleMint } from "../../generated/schema"; 9 | import { loadOrCreateAccount } from "../shared/accounts"; 10 | import { toETH } from "../shared/conversions"; 11 | import { loadOrCreateNFTContract } from "./nft"; 12 | import { loadOrCreateNFTMarketContract } from "./nftMarket"; 13 | 14 | export function handleBuyReferralPaid(event: BuyReferralPaid): void { 15 | // FixedPriceSale 16 | let fixedPriceSaleMint = FixedPriceSaleMint.load(event.transaction.hash.toHex()); 17 | if (fixedPriceSaleMint) { 18 | let fixedPriceSale = FixedPriceSale.load(fixedPriceSaleMint.fixedPriceSale); 19 | if (fixedPriceSale) { 20 | fixedPriceSaleMint.buyReferrer = loadOrCreateAccount(event.params.buyReferrer).id; 21 | fixedPriceSaleMint.buyReferrerFee = toETH(event.params.buyReferrerFee); 22 | if (event.params.buyReferrerFee) { 23 | if (fixedPriceSale.buyReferrerFee) { 24 | fixedPriceSale.buyReferrerFee = (fixedPriceSale.buyReferrerFee as BigDecimal).plus( 25 | toETH(event.params.buyReferrerFee), 26 | ); 27 | } else { 28 | fixedPriceSale.buyReferrerFee = toETH(event.params.buyReferrerFee); 29 | } 30 | } 31 | fixedPriceSaleMint.save(); 32 | fixedPriceSale.save(); 33 | } 34 | return; 35 | } 36 | } 37 | 38 | export function handleCreateFixedPriceSale(event: CreateFixedPriceSale): void { 39 | let fixedPriceSale = new FixedPriceSale(event.params.nftContract.toHex()); 40 | fixedPriceSale.nftMarketContract = loadOrCreateNFTMarketContract(event.address).id; 41 | fixedPriceSale.nftContract = loadOrCreateNFTContract(event.params.nftContract).id; 42 | fixedPriceSale.mintCount = BigInt.zero(); 43 | fixedPriceSale.seller = loadOrCreateAccount(event.params.seller).id; 44 | fixedPriceSale.unitPriceInETH = toETH(event.params.price); 45 | fixedPriceSale.limitPerAccount = event.params.limitPerAccount; 46 | fixedPriceSale.amountInETH = BigDecimal.zero(); 47 | fixedPriceSale.dateCreated = event.block.timestamp; 48 | fixedPriceSale.transactionHashCreated = event.transaction.hash; 49 | fixedPriceSale.creatorRevenueInETH = BigDecimal.zero(); 50 | fixedPriceSale.foundationRevenueInETH = BigDecimal.zero(); 51 | fixedPriceSale.foundationProtocolFeeInETH = BigDecimal.zero(); 52 | fixedPriceSale.buyReferrerFee = BigDecimal.zero(); 53 | fixedPriceSale.save(); 54 | } 55 | 56 | export function handleMintFromFixedPriceDrop(event: MintFromFixedPriceDrop): void { 57 | let fixedPriceSale = FixedPriceSale.load(event.params.nftContract.toHex()); 58 | if (fixedPriceSale) { 59 | let fixedPriceSaleMint = FixedPriceSaleMint.load(event.transaction.hash.toHex()); 60 | if (fixedPriceSaleMint) { 61 | fixedPriceSaleMint.fixedPriceSale = fixedPriceSale.id; 62 | fixedPriceSaleMint.buyer = loadOrCreateAccount(event.params.buyer).id; 63 | fixedPriceSaleMint.count = event.params.count; 64 | fixedPriceSaleMint.firstTokenId = event.params.firstTokenId; 65 | fixedPriceSaleMint.amountInETH = toETH(event.params.creatorRev).plus(toETH(event.params.totalFees)); 66 | 67 | // update Fixed Price Sale 68 | fixedPriceSale.mintCount = fixedPriceSale.mintCount.plus(event.params.count); 69 | fixedPriceSale.amountInETH = fixedPriceSale.amountInETH.plus(fixedPriceSaleMint.amountInETH as BigDecimal); 70 | fixedPriceSale.creatorRevenueInETH = fixedPriceSale.creatorRevenueInETH.plus(toETH(event.params.creatorRev)); 71 | fixedPriceSale.foundationRevenueInETH = fixedPriceSale.foundationRevenueInETH.plus(toETH(event.params.totalFees)); 72 | fixedPriceSale.foundationProtocolFeeInETH = fixedPriceSale.foundationProtocolFeeInETH.plus( 73 | toETH(event.params.totalFees).minus(fixedPriceSale.buyReferrerFee as BigDecimal), 74 | ); 75 | fixedPriceSale.latestMint = fixedPriceSaleMint.id; 76 | 77 | fixedPriceSaleMint.save(); 78 | fixedPriceSale.save(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /subgraph/src/mappings/percentSplit.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PercentSplitCreated as PercentSplitCreatedEvent, 3 | PercentSplitShare as PercentSplitShareEvent, 4 | } from "../../generated/PercentSplit/PercentSplitContract"; 5 | import { PercentSplit, PercentSplitShare } from "../../generated/schema"; 6 | import { PercentSplit as PercentSplitTemplate } from "../../generated/templates"; 7 | import { loadOrCreateAccount } from "../shared/accounts"; 8 | import { ONE_BIG_INT, ZERO_BIG_INT } from "../shared/constants"; 9 | import { toPercent } from "../shared/conversions"; 10 | 11 | export function handlePercentSplitCreated(event: PercentSplitCreatedEvent): void { 12 | let splitEntity = new PercentSplit(event.params.contractAddress.toHex()); 13 | splitEntity.shareCount = ZERO_BIG_INT; 14 | splitEntity.dateCreated = event.block.timestamp; 15 | splitEntity.save(); 16 | PercentSplitTemplate.create(event.params.contractAddress); 17 | } 18 | 19 | export function handlePercentSplitShare(event: PercentSplitShareEvent): void { 20 | let splitEntity = PercentSplit.load(event.address.toHex()); 21 | if (!splitEntity) { 22 | return; 23 | } 24 | 25 | let shareEntity = new PercentSplitShare(event.address.toHex() + "-" + event.logIndex.toString()); 26 | shareEntity.split = event.address.toHex(); 27 | shareEntity.account = loadOrCreateAccount(event.params.recipient).id; 28 | shareEntity.shareInPercent = toPercent(event.params.percentInBasisPoints); 29 | shareEntity.indexOfShare = splitEntity.shareCount; 30 | shareEntity.save(); 31 | 32 | splitEntity.shareCount = splitEntity.shareCount.plus(ONE_BIG_INT); 33 | splitEntity.save(); 34 | } 35 | -------------------------------------------------------------------------------- /subgraph/src/shared/accounts.ts: -------------------------------------------------------------------------------- 1 | import { Address } from "@graphprotocol/graph-ts"; 2 | 3 | import { Account } from "../../generated/schema"; 4 | import { ZERO_BIG_DECIMAL } from "./constants"; 5 | 6 | export function loadOrCreateAccount(address: Address): Account { 7 | let addressHex = address.toHex(); 8 | let account = Account.load(addressHex); 9 | if (!account) { 10 | account = new Account(addressHex); 11 | account.netRevenueInETH = ZERO_BIG_DECIMAL; 12 | account.netRevenuePendingInETH = ZERO_BIG_DECIMAL; 13 | account.save(); 14 | } 15 | return account as Account; 16 | } 17 | -------------------------------------------------------------------------------- /subgraph/src/shared/buyNow.ts: -------------------------------------------------------------------------------- 1 | import { Nft, NftMarketBuyNow } from "../../generated/schema"; 2 | 3 | export function loadLatestBuyNow(nft: Nft): NftMarketBuyNow | null { 4 | if (!nft.mostRecentBuyNow) { 5 | return null; 6 | } 7 | let buyNow = NftMarketBuyNow.load(nft.mostRecentBuyNow as string); 8 | if (!buyNow || buyNow.status != "Open") { 9 | return null; 10 | } 11 | return buyNow; 12 | } 13 | -------------------------------------------------------------------------------- /subgraph/src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | import { BigInt } from "@graphprotocol/graph-ts"; 2 | 3 | export const ZERO_BYTES_32_STRING = "0x0000000000000000000000000000000000000000000000000000000000000000"; 4 | export let ZERO_BIG_INT = BigInt.fromI32(0); 5 | export let ONE_BIG_INT = BigInt.fromI32(1); 6 | export let ZERO_BIG_DECIMAL = ZERO_BIG_INT.toBigDecimal(); 7 | export let BASIS_POINTS = BigInt.fromI32(10000); 8 | export let BASIS_POINTS_PER_PERCENT = BigInt.fromI32(100).toBigDecimal(); 9 | export let WEI_PER_ETH = BigInt.fromI32(10).pow(18).toBigDecimal(); 10 | export let ZERO_ADDRESS_STRING = "0x0000000000000000000000000000000000000000"; 11 | export let ONE_MINUTE = BigInt.fromI32(60); 12 | export let TEN_MINUTES = ONE_MINUTE.times(BigInt.fromI32(10)); 13 | -------------------------------------------------------------------------------- /subgraph/src/shared/conversions.ts: -------------------------------------------------------------------------------- 1 | import { BigDecimal, BigInt } from "@graphprotocol/graph-ts"; 2 | 3 | import { BASIS_POINTS_PER_PERCENT, WEI_PER_ETH } from "./constants"; 4 | 5 | export function toPercent(basisPoints: BigInt): BigDecimal { 6 | return basisPoints.toBigDecimal().div(BASIS_POINTS_PER_PERCENT); 7 | } 8 | 9 | export function toETH(wei: BigInt): BigDecimal { 10 | return wei.toBigDecimal().div(WEI_PER_ETH); 11 | } 12 | -------------------------------------------------------------------------------- /subgraph/src/shared/creators.ts: -------------------------------------------------------------------------------- 1 | import { Address } from "@graphprotocol/graph-ts"; 2 | 3 | import { Creator } from "../../generated/schema"; 4 | import { loadOrCreateAccount } from "./accounts"; 5 | import { ZERO_BIG_DECIMAL } from "./constants"; 6 | 7 | export function loadOrCreateCreator(address: Address): Creator { 8 | let account = loadOrCreateAccount(address); 9 | let creator = Creator.load(account.id); 10 | if (!creator) { 11 | creator = new Creator(account.id); 12 | creator.account = account.id; 13 | creator.netSalesInETH = ZERO_BIG_DECIMAL; 14 | creator.netSalesPendingInETH = ZERO_BIG_DECIMAL; 15 | creator.netRevenueInETH = ZERO_BIG_DECIMAL; 16 | creator.netRevenuePendingInETH = ZERO_BIG_DECIMAL; 17 | creator.save(); 18 | } 19 | return creator as Creator; 20 | } 21 | -------------------------------------------------------------------------------- /subgraph/src/shared/events.ts: -------------------------------------------------------------------------------- 1 | import { BigDecimal, BigInt, Bytes, ethereum, store } from "@graphprotocol/graph-ts"; 2 | 3 | import { 4 | Account, 5 | Nft, 6 | NftHistory, 7 | NftMarketAuction, 8 | NftMarketBuyNow, 9 | NftMarketOffer, 10 | PrivateSale, 11 | } from "../../generated/schema"; 12 | import { loadOrCreateAccount } from "./accounts"; 13 | import { ONE_BIG_INT, ZERO_BIG_INT } from "./constants"; 14 | import { getEventId, getPreviousEventId } from "./ids"; 15 | 16 | export function recordNftEvent( 17 | event: ethereum.Event, 18 | nft: Nft, 19 | eventType: string, 20 | actorAccount: Account, 21 | auction: NftMarketAuction | null = null, 22 | marketplace: string | null = null, 23 | amountInETH: BigDecimal | null = null, 24 | nftRecipient: Account | null = null, 25 | dateOverride: BigInt | null = null, 26 | amountInTokens: BigInt | null = null, 27 | tokenAddress: Bytes | null = null, 28 | privateSale: PrivateSale | null = null, 29 | offer: NftMarketOffer | null = null, 30 | buyNow: NftMarketBuyNow | null = null, 31 | ): void { 32 | let historicalEvent = new NftHistory(getEventId(event, eventType)); 33 | historicalEvent.nft = nft.id; 34 | historicalEvent.event = eventType; 35 | if (auction) { 36 | historicalEvent.auction = auction.id; 37 | } 38 | if (dateOverride) { 39 | historicalEvent.date = dateOverride as BigInt; 40 | } else { 41 | historicalEvent.date = event.block.timestamp; 42 | } 43 | historicalEvent.contractAddress = event.address; 44 | historicalEvent.transactionHash = event.transaction.hash; 45 | historicalEvent.actorAccount = actorAccount.id; 46 | historicalEvent.txOrigin = loadOrCreateAccount(event.transaction.from).id; 47 | if (nftRecipient) { 48 | historicalEvent.nftRecipient = nftRecipient.id; 49 | } 50 | historicalEvent.marketplace = marketplace; 51 | historicalEvent.amountInETH = amountInETH; 52 | historicalEvent.amountInTokens = amountInTokens; 53 | historicalEvent.tokenAddress = tokenAddress; 54 | if (privateSale) { 55 | historicalEvent.privateSale = privateSale.id; 56 | } 57 | if (offer) { 58 | historicalEvent.offer = offer.id; 59 | } 60 | if (buyNow) { 61 | historicalEvent.buyNow = buyNow.id; 62 | } 63 | historicalEvent.save(); 64 | } 65 | 66 | export function removePreviousTransferEvent(event: ethereum.Event): void { 67 | // There may be multiple logs that occurred since the last transfer event 68 | for (let i = event.logIndex.minus(ONE_BIG_INT); i.ge(ZERO_BIG_INT); i = i.minus(ONE_BIG_INT)) { 69 | let previousEvent = NftHistory.load(getPreviousEventId(event, "Transferred", i)); 70 | if (previousEvent) { 71 | store.remove("NftHistory", previousEvent.id); 72 | return; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /subgraph/src/shared/feth.ts: -------------------------------------------------------------------------------- 1 | import { Address, BigInt, ethereum } from "@graphprotocol/graph-ts"; 2 | 3 | import { Account, Feth, FethEscrow } from "../../generated/schema"; 4 | import { ZERO_BIG_DECIMAL } from "./constants"; 5 | 6 | interface EscrowEventParams { 7 | account: Address; 8 | amount: BigInt; 9 | expiration: BigInt; 10 | } 11 | 12 | interface EscrowEvent { 13 | params: EscrowEventParams; 14 | block: ethereum.Block; 15 | transaction: ethereum.Transaction; 16 | } 17 | 18 | export function getEscrowId(event: T): string { 19 | return event.params.account.toHex() + "-" + event.params.expiration.toString(); 20 | } 21 | 22 | export function loadOrCreateFeth(account: Account, block: ethereum.Block): Feth { 23 | let feth = Feth.load(account.id); 24 | if (!feth) { 25 | feth = new Feth(account.id); 26 | feth.user = account.id; 27 | feth.balanceInETH = ZERO_BIG_DECIMAL; 28 | feth.dateLastUpdated = block.timestamp; 29 | } 30 | return feth; 31 | } 32 | 33 | export function loadOrCreateFethEscrow(event: T, account: Account): FethEscrow { 34 | const escrowId = getEscrowId(event); 35 | let fethEscrow = FethEscrow.load(escrowId); 36 | if (!fethEscrow) { 37 | fethEscrow = new FethEscrow(escrowId); 38 | fethEscrow.transactionHashCreated = event.transaction.hash; 39 | fethEscrow.amountInETH = ZERO_BIG_DECIMAL; 40 | 41 | // Placeholder for expiry (now), this should be immediately replaced by the actual expiry 42 | fethEscrow.dateExpiry = event.params.expiration; 43 | } 44 | 45 | // Ensure that the escrow is associated with the account's FETH balance 46 | let fethAccount = loadOrCreateFeth(account, event.block); 47 | fethAccount.save(); 48 | fethEscrow.feth = fethAccount.id; 49 | 50 | return fethEscrow; 51 | } 52 | -------------------------------------------------------------------------------- /subgraph/src/shared/ids.ts: -------------------------------------------------------------------------------- 1 | import { BigInt, ethereum } from "@graphprotocol/graph-ts"; 2 | 3 | export function getLogId(event: ethereum.Event): string { 4 | return event.transaction.hash.toHex() + "-" + event.logIndex.toString(); 5 | } 6 | 7 | export function getEventId(event: ethereum.Event, eventType: string): string { 8 | return getLogId(event) + "-" + eventType; 9 | } 10 | 11 | export function getPreviousEventId(event: ethereum.Event, eventType: string, logIndex: BigInt): string { 12 | return event.transaction.hash.toHex() + "-" + logIndex.toString() + "-" + eventType; 13 | } 14 | -------------------------------------------------------------------------------- /subgraph/src/shared/offers.ts: -------------------------------------------------------------------------------- 1 | import { ethereum } from "@graphprotocol/graph-ts"; 2 | 3 | import { Account, Nft, NftMarketOffer } from "../../generated/schema"; 4 | import { recordNftEvent } from "./events"; 5 | 6 | export function loadLatestOffer(nft: Nft): NftMarketOffer | null { 7 | if (!nft.mostRecentOffer) { 8 | return null; 9 | } 10 | let offer = NftMarketOffer.load(nft.mostRecentOffer as string); 11 | if (!offer || offer.status != "Open") { 12 | return null; 13 | } 14 | return offer; 15 | } 16 | 17 | export function outbidOrExpirePreviousOffer( 18 | event: ethereum.Event, 19 | nft: Nft, 20 | newBuyer: Account, 21 | newOffer: NftMarketOffer, 22 | ): boolean { 23 | let offer = loadLatestOffer(nft); 24 | if (!offer) { 25 | // New offer 26 | return false; 27 | } 28 | 29 | let isExpired = offer.dateExpires.lt(event.block.timestamp); 30 | if (isExpired) { 31 | // Previous offer expired 32 | offer.status = "Expired"; 33 | offer.save(); 34 | let buyer = Account.load(offer.buyer) as Account; // Buyer was set on offer made 35 | recordNftEvent( 36 | event, 37 | nft, 38 | "OfferExpired", 39 | buyer, 40 | null, 41 | "Foundation", 42 | null, 43 | null, 44 | event.block.timestamp, 45 | null, 46 | null, 47 | null, 48 | offer, 49 | ); 50 | return false; 51 | } 52 | 53 | // Previous offer was outbid 54 | offer.status = "Outbid"; 55 | offer.dateOutbid = event.block.timestamp; 56 | offer.transactionHashOutbid = event.transaction.hash; 57 | offer.offerOutbidBy = newOffer.id; 58 | newOffer.outbidOffer = offer.id; 59 | offer.save(); 60 | 61 | let isIncreased = offer.buyer == newBuyer.id; 62 | return isIncreased; 63 | } 64 | -------------------------------------------------------------------------------- /subgraph/src/shared/revenue.ts: -------------------------------------------------------------------------------- 1 | import { BigDecimal } from "@graphprotocol/graph-ts"; 2 | 3 | import { Account, Creator, Nft } from "../../generated/schema"; 4 | 5 | export function recordSale( 6 | nft: Nft, 7 | seller: Account, 8 | creatorRevenueInETH: BigDecimal | null, 9 | ownerRevenueInETH: BigDecimal | null, 10 | foundationRevenueInETH: BigDecimal | null, 11 | ): void { 12 | if (!creatorRevenueInETH || !ownerRevenueInETH || !foundationRevenueInETH) { 13 | // This should never occur 14 | return; 15 | } 16 | let amountInETH = creatorRevenueInETH.plus(ownerRevenueInETH).plus(foundationRevenueInETH); 17 | 18 | // Creator revenue & sales 19 | let creator: Creator | null; 20 | if (nft.creator) { 21 | creator = Creator.load(nft.creator as string); 22 | } else { 23 | creator = null; 24 | } 25 | if (creator) { 26 | creator.netRevenueInETH = creator.netRevenueInETH.plus(creatorRevenueInETH); 27 | creator.netSalesInETH = creator.netSalesInETH.plus(amountInETH); 28 | creator.save(); 29 | } 30 | 31 | // Account revenue 32 | seller.netRevenueInETH = seller.netRevenueInETH.plus(ownerRevenueInETH); 33 | seller.save(); 34 | 35 | // NFT revenue & sales 36 | nft.netSalesInETH = nft.netSalesInETH.plus(amountInETH); 37 | nft.netRevenueInETH = nft.netRevenueInETH.plus(creatorRevenueInETH); 38 | nft.isFirstSale = false; 39 | nft.lastSalePriceInETH = amountInETH; 40 | nft.save(); 41 | } 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["es5", "es6", "ES2021.String"], 6 | "jsx": "react", 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "noImplicitAny": true, 10 | "outDir": "dist", 11 | "resolveJsonModule": true, 12 | "sourceMap": true, 13 | "strict": true, 14 | "target": "es5" 15 | }, 16 | "exclude": ["artifacts/**/*", "node_modules"], 17 | "include": [ 18 | "./deploy", 19 | "hardhat*.config.ts", 20 | "helpers", 21 | "scripts/**/*", 22 | "src/**/*", 23 | "subgraph/**/*", 24 | "tasks/**/*", 25 | "test/**/*", 26 | "test-supplemental/**/*", 27 | "versions", 28 | "subgraph/tests", 29 | "test-subgraph-integration", 30 | "test-mainnet-fork", 31 | "addresses.js", 32 | "test-gas-stories/**/*" 33 | ] 34 | } 35 | --------------------------------------------------------------------------------