├── .env.example ├── .gitignore ├── LICENSE.md ├── README.md ├── addresses ├── 1-mainnet.json ├── 10-optimism.json ├── 11155111-sepolia.json ├── 137-polygon.json ├── 31337-hardhat.json ├── 420-optimismGoerli.json ├── 42161-arbitrum.json ├── 421613-arbitrumGoerli.json ├── 5-goerli.json ├── 7777777-zora.json ├── 80001-polygonMumbai.json ├── 8453-base.json ├── 84531-baseGoerli.json └── 999-zoraGoerli.json ├── contracts ├── DCNT4907A.sol ├── DCNT721A.sol ├── DCNTCrescendo.sol ├── DCNTMetadataRenderer.sol ├── DCNTRegistry.sol ├── DCNTRentalMarket.sol ├── DCNTSDK.sol ├── DCNTSeries.sol ├── DCNTStaking.sol ├── DCNTVault.sol ├── DCNTVaultNFT.sol ├── FeeManager.sol ├── ZKEdition.sol ├── erc721a │ ├── ERC4907A.sol │ └── ERC721A.sol ├── extensions │ └── ERC1155Hooks.sol ├── interfaces │ ├── IBondingCurve.sol │ ├── IDCNTRegistry.sol │ ├── IDCNTSDK.sol │ ├── IDCNTSeries.sol │ ├── IFeeManager.sol │ ├── IMetadataRenderer.sol │ ├── IOnChainMetadata.sol │ ├── ISharedNFTLogic.sol │ └── ITokenWithBalance.sol ├── mocks │ ├── MockERC20.sol │ ├── MockERC721.sol │ └── MockV3Aggregator.sol ├── splits │ ├── SplitMain.sol │ ├── SplitWallet.sol │ ├── interfaces │ │ ├── ISplitMain.sol │ │ └── ReverseRecords.sol │ └── libraries │ │ └── Clones.sol ├── storage │ ├── CrescendoConfig.sol │ ├── DCNT721AStorage.sol │ ├── EditionConfig.sol │ ├── MetadataConfig.sol │ └── TokenGateConfig.sol └── utils │ ├── Credits.sol │ ├── MetadataRenderAdminCheck.sol │ ├── MusicMetadata.sol │ ├── OperatorFilterer.sol │ ├── Pausable.sol │ ├── SharedNFTLogic.sol │ ├── Splits.sol │ └── Version.sol ├── core └── index.ts ├── hardhat.config.ts ├── package-lock.json ├── package.json ├── scripts ├── deployDCNT4907A.ts ├── deployDCNT721A.ts ├── deployDCNTCrescendo.ts ├── deployDCNTMetadataRenderer.ts ├── deployDCNTRegistry.ts ├── deployDCNTRentalMarket.ts ├── deployDCNTSDK.ts ├── deployDCNTSeries.ts ├── deployDCNTStaking.ts ├── deployDCNTVault.ts ├── deployDCNTVaultNFT.ts ├── deployFeeManager.ts ├── deployZKEdition.ts ├── upgradeSDK.ts ├── verifyFeeManager.ts └── verifySDK.ts ├── tasks └── index.ts ├── test ├── DCNT4907a.ts ├── DCNT721a.ts ├── DCNTCrescendo.ts ├── DCNTRegistry.ts ├── DCNTRentalMarket.ts ├── DCNTSDK.ts ├── DCNTSeries.ts ├── DCNTStaking.ts ├── DCNTVault.ts ├── DCNTVaultNFT.ts ├── FeeManager.ts ├── Splits.ts └── ZKEditions.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | PRIVATE_KEY= 2 | COINMARKETCAP_KEY= 3 | ETHERSCAN_KEY= 4 | POLYGONSCAN_KEY= 5 | OPTIMISMSCAN_KEY= 6 | ARBISCAN_KEY= 7 | BLOCKSCOUT_KEY= 8 | BASESCAN_KEY= 9 | MAINNET_URL=https://mainnet.infura.io/v3/ 10 | GOERLI_URL=https://goerli.infura.io/v3/ 11 | SEPOLIA_URL=https://rpc.sepolia.org 12 | POLYGON_MAINNET_URL=https://polygon-rpc.com 13 | POLYGON_TESTNET_URL=https://rpc-mumbai.maticvigil.com 14 | OPTIMISM_MAINNET_URL=https://mainnet.optimism.io 15 | OPTIMISM_TESTNET_URL=https://goerli.optimism.io 16 | ARBITRUM_MAINNET_URL=https://arb1.arbitrum.io/rpc 17 | ARBITRUM_TESTNET_URL=https://goerli-rollup.arbitrum.io/rpc 18 | BASE_MAINNET_URL=https://mainnet.base.org 19 | BASE_TESTNET_URL=https://goerli.base.org 20 | ZORA_MAINNET_URL=https://mainnet.rpc.zora.energy 21 | ZORA_TESTNET_URL=https://testnet.rpc.zora.energy 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | coverage 4 | coverage.json 5 | typechain 6 | typechain-types 7 | 8 | #Hardhat files 9 | cache 10 | artifacts 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DecentSDK 2 | 3 | ### Getting Started: 4 | 5 | - `npm i` to install packages 6 | - copy `.env.example` to `.env` and add your variables 7 | - `npx hardhat run scripts/deployDCNTSDK.ts --network optimism_testnet` to deploy the DecentSDK 8 | 9 | ### Testing 10 | 11 | - `npm i` to install packages 12 | - `npx hardhat test` 13 | 14 | ### Prettying 15 | 16 | - `npm i` to install packages 17 | - `npx prettier --write 'contracts/**/*.sol'` 18 | 19 | ### Adding a new SDK Module 20 | 21 | 1. add contracts to `/contracts` folder 22 | 2. add interfaces to `/interfaces` folder 23 | 3. write deploy function in `core/index.js` 24 | 4. create an example interface to the sdk in `scripts/deploy{YOUR_CONTRACT_NAME}.ts` 25 | 5. test by running `npx hardhat run scripts/{YOUR_CONTRACT_NAME}.ts --network optimism_testnet` 26 | 27 | ### Verifying 28 | 29 | - `npx hardhat verify {CONTRACT_ADDRESS} --network {NETWORK}` 30 | - find full list of supported networks in `hardhat.config.js` 31 | -------------------------------------------------------------------------------- /addresses/1-mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0xBE41713a34c28DE907C045e64458AEB616b293C4", 3 | "DCNT721A": "0xE7Ae230f1B330866aAE93960305fca251EB5f0Fb", 4 | "DCNT4907A": "0x02389Ff7dB92D9b1063B64ce1FC0BDBA84185472", 5 | "DCNTSeries": "0x4E1CC2FED80fa764e05EaeDB807A9c2FC85e1fd9", 6 | "DCNTCrescendo": "0xa35E98971608803C4F4BcD876451bFf3f822CFE5", 7 | "DCNTVault": "0x36C3a2b8550558fe7Eb86541DaFed469CACd2Ff9", 8 | "DCNTStaking": "0x5392e06aC979e370fD45d25d0b5424cd8CA56529", 9 | "DCNTMetadataRenderer": "0x49799190aD4ef8299e0D078Eef07Bdb4309F7186", 10 | "DCNTRegistry": "0x79cC3C93e7bEc01E03EE3249e1A661dD09a1cBCD", 11 | "DCNTVaultNFT": "0x56f14A0f3874070fC950b50b59bE942Edc0B6aaF", 12 | "DCNTRentalMarket": "0xbed40B46f64d3d0bf74607b49744CE0182bE6cf0", 13 | "ZKEdition": "0x01E90044e2089a2D5D8e84EEd1139C2742BdF43E" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/10-optimism.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0xBE41713a34c28DE907C045e64458AEB616b293C4", 3 | "DCNT721A": "0xE7Ae230f1B330866aAE93960305fca251EB5f0Fb", 4 | "DCNT4907A": "0x02389Ff7dB92D9b1063B64ce1FC0BDBA84185472", 5 | "DCNTSeries": "0x4E1CC2FED80fa764e05EaeDB807A9c2FC85e1fd9", 6 | "DCNTCrescendo": "0xa35E98971608803C4F4BcD876451bFf3f822CFE5", 7 | "DCNTVault": "0x36C3a2b8550558fe7Eb86541DaFed469CACd2Ff9", 8 | "DCNTStaking": "0x5392e06aC979e370fD45d25d0b5424cd8CA56529", 9 | "DCNTMetadataRenderer": "0x49799190aD4ef8299e0D078Eef07Bdb4309F7186", 10 | "DCNTRegistry": "0x79cC3C93e7bEc01E03EE3249e1A661dD09a1cBCD", 11 | "DCNTVaultNFT": "0x56f14A0f3874070fC950b50b59bE942Edc0B6aaF", 12 | "DCNTRentalMarket": "0xbed40B46f64d3d0bf74607b49744CE0182bE6cf0", 13 | "ZKEdition": "0x01E90044e2089a2D5D8e84EEd1139C2742BdF43E" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/11155111-sepolia.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0xcBa1b717158264954a3F838B1F8b8D07e1Dc1920", 3 | "DCNT721A": "0x291681a7CB4C489cEa79C42De99986b984D52d91", 4 | "DCNT4907A": "0xbA061F9828B389268c8Bc79Fd42f98bBAA062096", 5 | "DCNTSeries": "0xFE50Be66E5a6643574a13C26b7039674b5fdaA34", 6 | "DCNTCrescendo": "0xEdb701ccff9846634f4D73ED21B71CaaDE0ea88E", 7 | "DCNTVault": "0x6660De08E651E63F09079B80bf4010Ecb7d64243", 8 | "DCNTStaking": "0xCcfF064c15E36bE681eE2807FCe8fe38176e8baa", 9 | "DCNTMetadataRenderer": "0xa7f09A12E056fF12d2528dEd8b4658649FFD509B", 10 | "DCNTRegistry": "0x8c68D952cbf05759Ed858D9A9E454Cc29932ca78", 11 | "DCNTVaultNFT": "0x1eFd877eB785685323F4cB3C11754Aeb79580739", 12 | "DCNTRentalMarket": "0x2983589C067B36078Ab65a603D9cE4BFba5e115c", 13 | "ZKEdition": "0x66db420B6E9bC8E63578D18fEe7270FA8cb47e2b" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/137-polygon.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0xBE41713a34c28DE907C045e64458AEB616b293C4", 3 | "DCNT721A": "0xE7Ae230f1B330866aAE93960305fca251EB5f0Fb", 4 | "DCNT4907A": "0x02389Ff7dB92D9b1063B64ce1FC0BDBA84185472", 5 | "DCNTSeries": "0x4E1CC2FED80fa764e05EaeDB807A9c2FC85e1fd9", 6 | "DCNTCrescendo": "0xa35E98971608803C4F4BcD876451bFf3f822CFE5", 7 | "DCNTVault": "0x36C3a2b8550558fe7Eb86541DaFed469CACd2Ff9", 8 | "DCNTStaking": "0x5392e06aC979e370fD45d25d0b5424cd8CA56529", 9 | "DCNTMetadataRenderer": "0x49799190aD4ef8299e0D078Eef07Bdb4309F7186", 10 | "DCNTRegistry": "0x79cC3C93e7bEc01E03EE3249e1A661dD09a1cBCD", 11 | "DCNTVaultNFT": "0x56f14A0f3874070fC950b50b59bE942Edc0B6aaF", 12 | "DCNTRentalMarket": "0xbed40B46f64d3d0bf74607b49744CE0182bE6cf0", 13 | "ZKEdition": "0x01E90044e2089a2D5D8e84EEd1139C2742BdF43E" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/31337-hardhat.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", 3 | "DCNT721A": "0x5FbDB2315678afecb367f032d93F642f64180aa3", 4 | "DCNT4907A": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", 5 | "DCNTSeries": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", 6 | "DCNTCrescendo": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", 7 | "DCNTVault": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", 8 | "DCNTStaking": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", 9 | "DCNTMetadataRenderer": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", 10 | "DCNTRegistry": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", 11 | "DCNTVaultNFT": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", 12 | "DCNTRentalMarket": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", 13 | "ZKEdition": "0x0165878A594ca255338adfa4d48449f69242Eb8F" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/420-optimismGoerli.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0x64a32A19748f0D10323Ba0120541e8927C532826", 3 | "DCNT721A": "0xF1f82B86D54d4b7c3263eC91784dE78F1e274A18", 4 | "DCNT4907A": "0x359aB5064460443aDafaf7B9a827FD2EbE39C451", 5 | "DCNTSeries": "0x01E90044e2089a2D5D8e84EEd1139C2742BdF43E", 6 | "DCNTCrescendo": "0x69A8E7Bf1850b1CF7dCDAcB743B836fe38625217", 7 | "DCNTVault": "0xF8ADcf659201aD12B71eC049B20cf85D5e8CdbEb", 8 | "DCNTStaking": "0x5B2af1730EFA60F72251D41cCAAc41dE69C2eE05", 9 | "DCNTMetadataRenderer": "0xD15141B6694BB5768ab80A0099B19Ef229e838bA", 10 | "DCNTRegistry": "0x86a6556899A2C9CC1BB5353367fcF11d0a243C95", 11 | "DCNTVaultNFT": "0x888F31188706a40223f3FA405bA398C4F798f773", 12 | "DCNTRentalMarket": "0xbDb11A55Ad1e6082F18aC0FAAdD971bb89A8640E", 13 | "ZKEdition": "0xA7254a3CB057DaB9bBD25ed9ef6E84F2833Fdb44" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/42161-arbitrum.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0xBE41713a34c28DE907C045e64458AEB616b293C4", 3 | "DCNT721A": "0xE7Ae230f1B330866aAE93960305fca251EB5f0Fb", 4 | "DCNT4907A": "0x02389Ff7dB92D9b1063B64ce1FC0BDBA84185472", 5 | "DCNTSeries": "0x4E1CC2FED80fa764e05EaeDB807A9c2FC85e1fd9", 6 | "DCNTCrescendo": "0xa35E98971608803C4F4BcD876451bFf3f822CFE5", 7 | "DCNTVault": "0x36C3a2b8550558fe7Eb86541DaFed469CACd2Ff9", 8 | "DCNTStaking": "0x5392e06aC979e370fD45d25d0b5424cd8CA56529", 9 | "DCNTMetadataRenderer": "0x49799190aD4ef8299e0D078Eef07Bdb4309F7186", 10 | "DCNTRegistry": "0x79cC3C93e7bEc01E03EE3249e1A661dD09a1cBCD", 11 | "DCNTVaultNFT": "0x56f14A0f3874070fC950b50b59bE942Edc0B6aaF", 12 | "DCNTRentalMarket": "0xbed40B46f64d3d0bf74607b49744CE0182bE6cf0", 13 | "ZKEdition": "0x01E90044e2089a2D5D8e84EEd1139C2742BdF43E" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/421613-arbitrumGoerli.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0x64a32A19748f0D10323Ba0120541e8927C532826", 3 | "DCNT721A": "0xF1f82B86D54d4b7c3263eC91784dE78F1e274A18", 4 | "DCNT4907A": "0x359aB5064460443aDafaf7B9a827FD2EbE39C451", 5 | "DCNTSeries": "0x01E90044e2089a2D5D8e84EEd1139C2742BdF43E", 6 | "DCNTCrescendo": "0x69A8E7Bf1850b1CF7dCDAcB743B836fe38625217", 7 | "DCNTVault": "0x36C3a2b8550558fe7Eb86541DaFed469CACd2Ff9", 8 | "DCNTStaking": "0x5392e06aC979e370fD45d25d0b5424cd8CA56529", 9 | "DCNTMetadataRenderer": "0x49799190aD4ef8299e0D078Eef07Bdb4309F7186", 10 | "DCNTRegistry": "0x79cC3C93e7bEc01E03EE3249e1A661dD09a1cBCD", 11 | "DCNTVaultNFT": "0x888F31188706a40223f3FA405bA398C4F798f773", 12 | "DCNTRentalMarket": "0xbed40B46f64d3d0bf74607b49744CE0182bE6cf0", 13 | "ZKEdition": "0xA7254a3CB057DaB9bBD25ed9ef6E84F2833Fdb44" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/5-goerli.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0x64a32A19748f0D10323Ba0120541e8927C532826", 3 | "DCNT721A": "0xF1f82B86D54d4b7c3263eC91784dE78F1e274A18", 4 | "DCNT4907A": "0x359aB5064460443aDafaf7B9a827FD2EbE39C451", 5 | "DCNTSeries": "0x01E90044e2089a2D5D8e84EEd1139C2742BdF43E", 6 | "DCNTCrescendo": "0x69A8E7Bf1850b1CF7dCDAcB743B836fe38625217", 7 | "DCNTVault": "0x36C3a2b8550558fe7Eb86541DaFed469CACd2Ff9", 8 | "DCNTStaking": "0x5392e06aC979e370fD45d25d0b5424cd8CA56529", 9 | "DCNTMetadataRenderer": "0x49799190aD4ef8299e0D078Eef07Bdb4309F7186", 10 | "DCNTRegistry": "0x79cC3C93e7bEc01E03EE3249e1A661dD09a1cBCD", 11 | "DCNTVaultNFT": "0x888F31188706a40223f3FA405bA398C4F798f773", 12 | "DCNTRentalMarket": "0xbed40B46f64d3d0bf74607b49744CE0182bE6cf0", 13 | "ZKEdition": "0xA7254a3CB057DaB9bBD25ed9ef6E84F2833Fdb44" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/7777777-zora.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0x8c68D952cbf05759Ed858D9A9E454Cc29932ca78", 3 | "DCNT721A": "0x0c2698D597752E400ecC7f270be0A6fdc2bBe1A1", 4 | "DCNT4907A": "0x291681a7CB4C489cEa79C42De99986b984D52d91", 5 | "DCNTSeries": "0xbA061F9828B389268c8Bc79Fd42f98bBAA062096", 6 | "DCNTCrescendo": "0xFE50Be66E5a6643574a13C26b7039674b5fdaA34", 7 | "DCNTVault": "0xEdb701ccff9846634f4D73ED21B71CaaDE0ea88E", 8 | "DCNTStaking": "0x6660De08E651E63F09079B80bf4010Ecb7d64243", 9 | "DCNTMetadataRenderer": "0x327793Fa255bdD63C20a4aAD11c3A944A1EA62d6", 10 | "DCNTRegistry": "0xa7f09A12E056fF12d2528dEd8b4658649FFD509B", 11 | "DCNTVaultNFT": "0xcBa1b717158264954a3F838B1F8b8D07e1Dc1920", 12 | "DCNTRentalMarket": "0x1eFd877eB785685323F4cB3C11754Aeb79580739", 13 | "ZKEdition": "0xCcfF064c15E36bE681eE2807FCe8fe38176e8baa" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/80001-polygonMumbai.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0x64a32A19748f0D10323Ba0120541e8927C532826", 3 | "DCNT721A": "0xF1f82B86D54d4b7c3263eC91784dE78F1e274A18", 4 | "DCNT4907A": "0x359aB5064460443aDafaf7B9a827FD2EbE39C451", 5 | "DCNTSeries": "0x01E90044e2089a2D5D8e84EEd1139C2742BdF43E", 6 | "DCNTCrescendo": "0x69A8E7Bf1850b1CF7dCDAcB743B836fe38625217", 7 | "DCNTVault": "0xd2d92F35F52Ec65d39C136ebD357004472bDc758", 8 | "DCNTStaking": "0xbed40B46f64d3d0bf74607b49744CE0182bE6cf0", 9 | "DCNTMetadataRenderer": "0x815Cf686109C8591536F921920d5BF357E67d7F5", 10 | "DCNTRegistry": "0x0d335a3F2284C974a70afdcAb1dB4D00B6c82079", 11 | "DCNTVaultNFT": "0x888F31188706a40223f3FA405bA398C4F798f773", 12 | "DCNTRentalMarket": "0x86ca89c04e6E09ef837efcaAf74805686Fe3B349", 13 | "ZKEdition": "0xA7254a3CB057DaB9bBD25ed9ef6E84F2833Fdb44" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/8453-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0x8c68D952cbf05759Ed858D9A9E454Cc29932ca78", 3 | "DCNT721A": "0x0c2698D597752E400ecC7f270be0A6fdc2bBe1A1", 4 | "DCNT4907A": "0x291681a7CB4C489cEa79C42De99986b984D52d91", 5 | "DCNTSeries": "0xbA061F9828B389268c8Bc79Fd42f98bBAA062096", 6 | "DCNTCrescendo": "0xFE50Be66E5a6643574a13C26b7039674b5fdaA34", 7 | "DCNTVault": "0xEdb701ccff9846634f4D73ED21B71CaaDE0ea88E", 8 | "DCNTStaking": "0x6660De08E651E63F09079B80bf4010Ecb7d64243", 9 | "DCNTMetadataRenderer": "0x327793Fa255bdD63C20a4aAD11c3A944A1EA62d6", 10 | "DCNTRegistry": "0xa7f09A12E056fF12d2528dEd8b4658649FFD509B", 11 | "DCNTVaultNFT": "0xcBa1b717158264954a3F838B1F8b8D07e1Dc1920", 12 | "DCNTRentalMarket": "0x1eFd877eB785685323F4cB3C11754Aeb79580739", 13 | "ZKEdition": "0xCcfF064c15E36bE681eE2807FCe8fe38176e8baa" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/84531-baseGoerli.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0x8c68D952cbf05759Ed858D9A9E454Cc29932ca78", 3 | "DCNT721A": "0x0c2698D597752E400ecC7f270be0A6fdc2bBe1A1", 4 | "DCNT4907A": "0x291681a7CB4C489cEa79C42De99986b984D52d91", 5 | "DCNTSeries": "0xbA061F9828B389268c8Bc79Fd42f98bBAA062096", 6 | "DCNTCrescendo": "0xFE50Be66E5a6643574a13C26b7039674b5fdaA34", 7 | "DCNTVault": "0xEdb701ccff9846634f4D73ED21B71CaaDE0ea88E", 8 | "DCNTStaking": "0x6660De08E651E63F09079B80bf4010Ecb7d64243", 9 | "DCNTMetadataRenderer": "0x327793Fa255bdD63C20a4aAD11c3A944A1EA62d6", 10 | "DCNTRegistry": "0xa7f09A12E056fF12d2528dEd8b4658649FFD509B", 11 | "DCNTVaultNFT": "0xcBa1b717158264954a3F838B1F8b8D07e1Dc1920", 12 | "DCNTRentalMarket": "0x1eFd877eB785685323F4cB3C11754Aeb79580739", 13 | "ZKEdition": "0xCcfF064c15E36bE681eE2807FCe8fe38176e8baa" 14 | } 15 | -------------------------------------------------------------------------------- /addresses/999-zoraGoerli.json: -------------------------------------------------------------------------------- 1 | { 2 | "DCNTSDK": "0xcBa1b717158264954a3F838B1F8b8D07e1Dc1920", 3 | "DCNT721A": "0x291681a7CB4C489cEa79C42De99986b984D52d91", 4 | "DCNT4907A": "0xbA061F9828B389268c8Bc79Fd42f98bBAA062096", 5 | "DCNTSeries": "0xFE50Be66E5a6643574a13C26b7039674b5fdaA34", 6 | "DCNTCrescendo": "0xEdb701ccff9846634f4D73ED21B71CaaDE0ea88E", 7 | "DCNTVault": "0x6660De08E651E63F09079B80bf4010Ecb7d64243", 8 | "DCNTStaking": "0xCcfF064c15E36bE681eE2807FCe8fe38176e8baa", 9 | "DCNTMetadataRenderer": "0xa7f09A12E056fF12d2528dEd8b4658649FFD509B", 10 | "DCNTRegistry": "0x8c68D952cbf05759Ed858D9A9E454Cc29932ca78", 11 | "DCNTVaultNFT": "0x1eFd877eB785685323F4cB3C11754Aeb79580739", 12 | "DCNTRentalMarket": "0x2983589C067B36078Ab65a603D9cE4BFba5e115c", 13 | "ZKEdition": "0x66db420B6E9bC8E63578D18fEe7270FA8cb47e2b" 14 | } 15 | -------------------------------------------------------------------------------- /contracts/DCNT4907A.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | 18 | import "./DCNT721A.sol"; 19 | import "./erc721a/ERC4907A.sol"; 20 | 21 | /// @title template NFT contract 22 | contract DCNT4907A is DCNT721A, ERC4907A { 23 | /// @dev we must override _baseURI because it is implemented in both 721A contracts 24 | function _baseURI() 25 | internal 26 | view 27 | override(DCNT721A, ERC721A) 28 | returns (string memory) 29 | { 30 | return baseURI; 31 | } 32 | 33 | function supportsInterface(bytes4 interfaceId) 34 | public 35 | view 36 | virtual 37 | override(DCNT721A, ERC4907A) 38 | returns (bool) 39 | { 40 | return super.supportsInterface(interfaceId); 41 | } 42 | 43 | function tokenURI(uint256 tokenId) 44 | public 45 | view 46 | virtual 47 | override(DCNT721A, ERC721A, IERC721A) 48 | returns (string memory) 49 | { 50 | return DCNT721A.tokenURI(tokenId); 51 | } 52 | 53 | /// @dev we must specify inheritance path for _beforeTokenTransfers 54 | function _beforeTokenTransfers( 55 | address from, 56 | address to, 57 | uint256 startTokenId, 58 | uint256 quantity 59 | ) internal virtual override(DCNT721A, ERC721A) { 60 | DCNT721A._beforeTokenTransfers(from, to, startTokenId, quantity); 61 | } 62 | 63 | /// @dev we must specify inheritance path for setApprovalForAll 64 | function setApprovalForAll( 65 | address operator, 66 | bool approved 67 | ) public override(DCNT721A, ERC721A, IERC721A) { 68 | DCNT721A.setApprovalForAll(operator, approved); 69 | } 70 | 71 | /// @dev we must specify inheritance path for approve 72 | function approve( 73 | address operator, 74 | uint256 tokenId 75 | ) public override(DCNT721A, ERC721A, IERC721A) { 76 | DCNT721A.approve(operator, tokenId); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /contracts/DCNTCrescendo.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | 18 | import "solmate/src/tokens/ERC1155.sol"; 19 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 20 | import "@openzeppelin/contracts/access/Ownable.sol"; 21 | 22 | import "./interfaces/IBondingCurve.sol"; 23 | import "./interfaces/IMetadataRenderer.sol"; 24 | 25 | import "./storage/CrescendoConfig.sol"; 26 | import "./storage/MetadataConfig.sol"; 27 | import "./utils/Splits.sol"; 28 | import './utils/Version.sol'; 29 | 30 | /// ========= Bonding Token ========= 31 | 32 | contract DCNTCrescendo is 33 | IBondingCurve, 34 | ERC1155, 35 | Initializable, 36 | Ownable, 37 | Splits, 38 | Version(3) 39 | { 40 | // Token name 41 | string private _name; 42 | 43 | // Token symbol 44 | string private _symbol; 45 | 46 | // Token uri 47 | string private _uri; 48 | string private _contractURI; 49 | 50 | uint256 public step1; 51 | uint256 public step2; 52 | uint256 public hitch; 53 | uint256 public takeRateBPS; 54 | uint256 public royaltyBPS; 55 | 56 | // id to supply 57 | mapping(uint256 => uint256) private _totalSupply; 58 | // id to current price 59 | mapping(uint256 => uint256) private _currentPrice; 60 | 61 | uint256 public totalWithdrawn = 0; 62 | 63 | uint256 public saleStart; 64 | bool public saleIsPaused; 65 | 66 | uint256 public unlockDate; 67 | 68 | uint256 constant bps = 100_00; 69 | 70 | /// @notice DCNTMetadataRenderer address 71 | address public metadataRenderer; 72 | 73 | address public parentIP; 74 | /// ============ Constructor ============ 75 | 76 | function initialize( 77 | address _owner, 78 | // string memory name_, 79 | // string memory symbol_, 80 | // string memory uri_, 81 | CrescendoConfig memory _config, 82 | MetadataConfig memory _metadataConfig, 83 | address _metadataRenderer 84 | ) public initializer { 85 | _transferOwnership(_owner); 86 | _currentPrice[0] = _config.initialPrice; 87 | step1 = _config.step1; 88 | step2 = _config.step2; 89 | hitch = _config.hitch; 90 | takeRateBPS = _config.takeRateBPS; 91 | unlockDate = _config.unlockDate; 92 | _name = _config.name; 93 | _symbol = _config.symbol; 94 | saleStart = _config.saleStart; 95 | royaltyBPS = _config.royaltyBPS; 96 | parentIP = _metadataConfig.parentIP; 97 | 98 | if ( 99 | _metadataRenderer != address(0) && 100 | _metadataConfig.metadataRendererInit.length > 0 101 | ) { 102 | metadataRenderer = _metadataRenderer; 103 | IMetadataRenderer(_metadataRenderer).initializeWithData( 104 | _metadataConfig.metadataRendererInit 105 | ); 106 | } else { 107 | _contractURI = _metadataConfig.contractURI; 108 | _setURI(_metadataConfig.metadataURI); 109 | } 110 | } 111 | 112 | function calculateCurvedMintReturn(uint256 amount, uint256 id) 113 | public 114 | view 115 | override 116 | returns (uint256) 117 | { 118 | require(amount == 1, "max amount is 1"); 119 | return _currentPrice[id]; 120 | } 121 | 122 | function calculateCurvedBurnReturn(uint256 amount, uint256 id) 123 | public 124 | view 125 | override 126 | returns (uint256) 127 | { 128 | require(amount == 1, "max amount is 1"); 129 | return ((bps - takeRateBPS) * _currentPrice[id]) / bps; 130 | } 131 | 132 | modifier salesAreActive { 133 | require(block.timestamp >= saleStart, "Sales are not active yet."); 134 | require(block.timestamp < unlockDate, "Sales are no longer active."); 135 | require(! saleIsPaused, "Sale must be active to buy or sell"); 136 | _; 137 | } 138 | 139 | function saleIsActive() external view returns(bool _saleIsActive) { 140 | _saleIsActive = (block.timestamp >= saleStart) && (!saleIsPaused); 141 | } 142 | 143 | /// @notice purchase nft 144 | function buy(uint256 id) external payable salesAreActive { 145 | uint256 price = calculateCurvedMintReturn(1, id); 146 | require(msg.value >= price, "Insufficient funds"); 147 | require(id == 0, "currently only one edition"); 148 | 149 | // allow for slippage 150 | if (msg.value - price > 0) { 151 | (bool success, ) = payable(msg.sender).call{value: (msg.value - price)}( 152 | "" 153 | ); 154 | require(success, "Failed to send ether"); 155 | } 156 | 157 | _mint(msg.sender, id, 1, ""); 158 | _totalSupply[id] += 1; 159 | emit CurvedMint(msg.sender, 1, id, price); 160 | 161 | // update supply / price 162 | if (totalSupply(id) < hitch) { 163 | _currentPrice[id] += step1; 164 | } else { 165 | _currentPrice[id] += step2; 166 | } 167 | } 168 | 169 | /// @notice sell nft if liquidity is available 170 | function sell(uint256 id) external salesAreActive { 171 | require(id == 0, "currently only one edition"); 172 | uint256 price = calculateCurvedBurnReturn(1, id); 173 | require(balanceOf[msg.sender][id] > 0, "must own nft to sell"); 174 | 175 | // burn nft 176 | _burn(msg.sender, id, 1); 177 | _totalSupply[id] -= 1; 178 | 179 | // send money to nft holder 180 | (bool success, ) = payable(msg.sender).call{value: price}(""); 181 | require(success, "Failed to send ether"); 182 | emit CurvedBurn(msg.sender, 1, id, price); 183 | 184 | // update supply / price 185 | if (totalSupply(id) < hitch) { 186 | _currentPrice[id] -= step1; 187 | } else { 188 | _currentPrice[id] -= step2; 189 | } 190 | } 191 | 192 | function totalSupply(uint256 id) public view returns (uint256) { 193 | return _totalSupply[id]; 194 | } 195 | 196 | function flipSaleState() external onlyOwner { 197 | saleIsPaused = !saleIsPaused; 198 | } 199 | 200 | function withdrawFund() external onlyOwner onlyUnlocked { 201 | require( 202 | splitWallet == address(0), 203 | "Cannot withdraw with an active split" 204 | ); 205 | (bool success, ) = payable(msg.sender).call{value: address(this).balance}(""); 206 | require(success, "Could not withdraw fund"); 207 | } 208 | 209 | /// @notice only when crescendo is unlocked 210 | modifier onlyUnlocked() { 211 | require(block.timestamp >= unlockDate, "Crescendo is still locked"); 212 | _; 213 | } 214 | 215 | function distributeAndWithdrawFund( 216 | address account, 217 | uint256 withdrawETH, 218 | ERC20[] memory tokens, 219 | address[] calldata accounts, 220 | uint32[] calldata percentAllocations, 221 | uint32 distributorFee, 222 | address distributorAddress 223 | ) public virtual requireSplit onlyUnlocked { 224 | if (withdrawETH != 0) { 225 | super._transferETHToSplit(); 226 | ISplitMain(splitMain).distributeETH( 227 | splitWallet, 228 | accounts, 229 | percentAllocations, 230 | distributorFee, 231 | distributorAddress 232 | ); 233 | } 234 | 235 | for (uint256 i = 0; i < tokens.length; ++i) { 236 | distributeERC20( 237 | tokens[i], 238 | accounts, 239 | percentAllocations, 240 | distributorFee, 241 | distributorAddress 242 | ); 243 | } 244 | 245 | _withdraw(account, withdrawETH, tokens); 246 | } 247 | 248 | function withdraw() external onlyOwner { 249 | require( 250 | splitWallet == address(0), 251 | "Cannot withdraw with an active split" 252 | ); 253 | uint256 toWithdraw = liquidity() - totalWithdrawn; 254 | totalWithdrawn += toWithdraw; 255 | (bool success, ) = payable(msg.sender).call{value: toWithdraw}(""); 256 | require(success, "Failed to send ether"); 257 | } 258 | 259 | function transferFundToSplit(uint256 transferETH, ERC20[] memory tokens) 260 | public 261 | virtual 262 | requireSplit 263 | onlyUnlocked 264 | { 265 | if (transferETH != 0) { 266 | super._transferETHToSplit(); 267 | } 268 | 269 | for (uint256 i = 0; i < tokens.length; ++i) { 270 | _transferERC20ToSplit(tokens[i]); 271 | } 272 | } 273 | 274 | function _transferETHToSplit() internal override { 275 | uint256 toWithdraw = liquidity() - totalWithdrawn; 276 | totalWithdrawn += toWithdraw; 277 | (bool success, ) = splitWallet.call{value: toWithdraw}(""); 278 | require(success, "Could not transfer ETH to split"); 279 | } 280 | 281 | function liquidity() public view returns (uint256) { 282 | return (takeRateBPS * (address(this).balance + totalWithdrawn)) / bps; 283 | } 284 | 285 | function reserveAmt() public view returns (uint256) { 286 | return 287 | ((bps - takeRateBPS) * (address(this).balance + totalWithdrawn)) / bps; 288 | } 289 | 290 | function name() external view returns (string memory) { 291 | return _name; 292 | } 293 | 294 | function symbol() external view returns (string memory) { 295 | return _symbol; 296 | } 297 | 298 | function contractURI() public view returns (string memory) { 299 | if (metadataRenderer != address(0)) { 300 | return IMetadataRenderer(metadataRenderer).contractURI(); 301 | } 302 | return _contractURI; 303 | } 304 | 305 | function updateContractURI(string memory contractURI_) external onlyOwner { 306 | _contractURI = contractURI_; 307 | } 308 | 309 | function uri(uint256) public view override returns (string memory) { 310 | if (metadataRenderer != address(0)) { 311 | return IMetadataRenderer(metadataRenderer).tokenURI(1); 312 | } 313 | return _uri; 314 | } 315 | 316 | function _setURI(string memory newuri) private { 317 | _uri = newuri; 318 | } 319 | 320 | function updateUri(string memory uri_) external onlyOwner { 321 | _setURI(uri_); 322 | } 323 | 324 | function setMetadataRenderer(address _metadataRenderer) external onlyOwner { 325 | metadataRenderer = _metadataRenderer; 326 | } 327 | 328 | function royaltyInfo(uint256 tokenId, uint256 salePrice) 329 | external 330 | view 331 | returns (address receiver, uint256 royaltyAmount) 332 | { 333 | require(tokenId == 0, "currently only one edition"); 334 | 335 | if (splitWallet != address(0)) { 336 | receiver = splitWallet; 337 | } else { 338 | receiver = owner(); 339 | } 340 | 341 | uint256 royaltyPayment = (salePrice * royaltyBPS) / bps; 342 | 343 | return (receiver, royaltyPayment); 344 | } 345 | 346 | function supportsInterface(bytes4 interfaceId) 347 | public 348 | view 349 | virtual 350 | override(ERC1155) 351 | returns (bool) 352 | { 353 | return 354 | interfaceId == 0x2a55205a || // ERC2981 interface ID for ERC2981. 355 | super.supportsInterface(interfaceId); 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /contracts/DCNTMetadataRenderer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | import "./interfaces/IMetadataRenderer.sol"; 18 | import {MusicMetadata} from "./utils/MusicMetadata.sol"; 19 | import {Credits} from "./utils/Credits.sol"; 20 | import {ISharedNFTLogic} from "./interfaces/ISharedNFTLogic.sol"; 21 | import "erc721a/contracts/IERC721A.sol"; 22 | 23 | /// @notice DCNTMetadataRenderer for editions support 24 | contract DCNTMetadataRenderer is IMetadataRenderer, MusicMetadata, Credits { 25 | /// @notice Reference to Shared NFT logic library 26 | ISharedNFTLogic public immutable sharedNFTLogic; 27 | 28 | /// @notice Constructor for library 29 | /// @param _sharedNFTLogic reference to shared NFT logic library 30 | constructor(ISharedNFTLogic _sharedNFTLogic) { 31 | sharedNFTLogic = _sharedNFTLogic; 32 | } 33 | 34 | /// @notice Default initializer for edition data from a specific contract 35 | /// @param data data to init with 36 | function initializeWithData(bytes memory data) external { 37 | // data format: description, imageURI, animationURI 38 | ( 39 | string memory description, 40 | string memory imageURI, 41 | string memory animationURI 42 | ) = abi.decode(data, (string, string, string)); 43 | 44 | songMetadatas[msg.sender].songPublishingData.description = description; 45 | songMetadatas[msg.sender].song.audio.losslessAudio = animationURI; 46 | songMetadatas[msg.sender].song.artwork.artworkUri = imageURI; 47 | 48 | emit EditionInitialized({ 49 | target: msg.sender, 50 | description: description, 51 | imageURI: imageURI, 52 | animationURI: animationURI 53 | }); 54 | } 55 | 56 | /// @notice Update everything in 1 transaction. 57 | /// @param target target for contract to update metadata for 58 | /// @param _songMetadata song metadata 59 | /// @param _projectMetadata project metadata 60 | /// @param _tags tags 61 | /// @param _credits credits for the track 62 | function bulkUpdate( 63 | address target, 64 | SongMetadata memory _songMetadata, 65 | ProjectMetadata memory _projectMetadata, 66 | string[] memory _tags, 67 | Credit[] calldata _credits 68 | ) external requireSenderAdmin(target) { 69 | songMetadatas[target] = _songMetadata; 70 | projectMetadatas[target] = _projectMetadata; 71 | updateTags(target, _tags); 72 | updateCredits(target, _credits); 73 | 74 | emit SongUpdated({ 75 | target: target, 76 | sender: msg.sender, 77 | songMetadata: _songMetadata, 78 | projectMetadata: _projectMetadata, 79 | tags: _tags, 80 | credits: _credits 81 | }); 82 | } 83 | 84 | /// @notice Contract URI information getter 85 | /// @return contract uri (if set) 86 | function contractURI() external view override returns (string memory) { 87 | address target = msg.sender; 88 | bytes memory imageSpace = bytes(""); 89 | if (bytes(songMetadatas[target].song.artwork.artworkUri).length > 0) { 90 | imageSpace = abi.encodePacked( 91 | '", "image": "', 92 | songMetadatas[target].song.artwork.artworkUri 93 | ); 94 | } 95 | bool isMusicNft = bytes( 96 | songMetadatas[target].song.audio.songDetails.audioQuantitative.audioMimeType 97 | ).length > 0; 98 | string memory name = isMusicNft 99 | ? songMetadatas[target].songPublishingData.title 100 | : IERC721A(target).name(); 101 | return 102 | string( 103 | sharedNFTLogic.encodeMetadataJSON( 104 | abi.encodePacked( 105 | '{"name": "', 106 | name, 107 | '", "description": "', 108 | songMetadatas[target].songPublishingData.description, 109 | imageSpace, 110 | '"}' 111 | ) 112 | ) 113 | ); 114 | } 115 | 116 | /// @notice Token URI information getter 117 | /// @param tokenId to get uri for 118 | /// @return contract uri (if set) 119 | function tokenURI(uint256 tokenId) 120 | external 121 | view 122 | override 123 | returns (string memory) 124 | { 125 | address target = msg.sender; 126 | 127 | return tokenURITarget(tokenId, target); 128 | } 129 | 130 | /// @notice Token URI information getter 131 | /// @param tokenId to get uri for 132 | /// @return contract uri (if set) 133 | function tokenURITarget(uint256 tokenId, address target) 134 | public 135 | view 136 | returns (string memory) 137 | { 138 | return 139 | sharedNFTLogic.createMetadataEdition({ 140 | name: IERC721A(target).name(), 141 | tokenOfEdition: tokenId, 142 | songMetadata: songMetadatas[target], 143 | projectMetadata: projectMetadatas[target], 144 | credits: credits[target], 145 | tags: trackTags[target] 146 | }); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /contracts/DCNTRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | 18 | import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 19 | 20 | contract DCNTRegistry { 21 | using EnumerableSet for EnumerableSet.AddressSet; 22 | 23 | mapping(address => EnumerableSet.AddressSet) private contracts; 24 | 25 | /// ============ Events ============ 26 | 27 | event Register( 28 | address indexed deployer, 29 | address indexed deployment, 30 | string key 31 | ); 32 | 33 | event Remove(address indexed deployer, address indexed deployment); 34 | 35 | /// ============ Constructor ============ 36 | 37 | constructor() {} 38 | 39 | /// ============ Functions ============ 40 | 41 | function register( 42 | address _deployer, 43 | address _deployment, 44 | string calldata _key 45 | ) external { 46 | bool registered = contracts[_deployer].add(_deployment); 47 | require(registered, "Registration was unsuccessful"); 48 | emit Register(_deployer, _deployment, _key); 49 | } 50 | 51 | function remove(address _deployer, address _deployment) external { 52 | bool removed = contracts[_deployer].remove(_deployment); 53 | require(removed, "Removal was unsuccessful"); 54 | emit Remove(_deployer, _deployment); 55 | } 56 | 57 | function query(address _deployer) external view returns (address[] memory) { 58 | return contracts[_deployer].values(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /contracts/DCNTRentalMarket.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | 18 | import "erc721a/contracts/extensions/IERC4907A.sol"; 19 | import "@openzeppelin/contracts/interfaces/IERC2981.sol"; 20 | 21 | contract DCNTRentalMarket { 22 | 23 | struct Rentable { 24 | bool isListed; 25 | uint16 minDays; 26 | uint16 maxDays; 27 | uint256 pricePerDay; 28 | } 29 | 30 | struct RentableConfig { 31 | address nft; 32 | uint256 tokenId; 33 | bool isListed; 34 | uint256 pricePerDay; 35 | uint16 minDays; 36 | uint16 maxDays; 37 | } 38 | 39 | /// @notice mapping of rental listings by token and index 40 | /// @dev nft => tokenId => Rentable 41 | mapping(address => mapping(uint256 => Rentable)) public rentables; 42 | 43 | event SetRentable( 44 | address indexed nft, 45 | uint256 indexed tokenId, 46 | Rentable rentable 47 | ); 48 | 49 | event Rent( 50 | address indexed nft, 51 | uint256 indexed tokenId, 52 | address indexed user, 53 | uint64 expires 54 | ); 55 | 56 | constructor() { } 57 | 58 | function setRentable( 59 | address _nft, 60 | uint256 _tokenId, 61 | bool _isListed, 62 | uint256 _pricePerDay, 63 | uint16 _minDays, 64 | uint16 _maxDays 65 | ) external { 66 | _setRentable(_nft, _tokenId, _isListed, _pricePerDay, _minDays, _maxDays); 67 | } 68 | 69 | function setRentables(RentableConfig[] calldata _rentableConfigs) external { 70 | uint256 length = _rentableConfigs.length; 71 | for (uint i = 0; i < length; i++) { 72 | RentableConfig memory rentableConfig = _rentableConfigs[i]; 73 | _setRentable( 74 | rentableConfig.nft, 75 | rentableConfig.tokenId, 76 | rentableConfig.isListed, 77 | rentableConfig.pricePerDay, 78 | rentableConfig.minDays, 79 | rentableConfig.maxDays 80 | ); 81 | } 82 | } 83 | 84 | function _setRentable( 85 | address _nft, 86 | uint256 _tokenId, 87 | bool _isListed, 88 | uint256 _pricePerDay, 89 | uint16 _minDays, 90 | uint16 _maxDays 91 | ) private { 92 | address tokenOwner = IERC4907A(_nft).ownerOf(_tokenId); 93 | require(IERC4907A(_nft).supportsInterface(0xad092b5c), 'NFT does not support ERC4907'); 94 | require(_checkApproved(_nft, _tokenId, tokenOwner), 'Token is not approved for rentals'); 95 | require(IERC4907A(_nft).ownerOf(_tokenId) == msg.sender, 'Must be token owner to set rentable'); 96 | 97 | rentables[_nft][_tokenId] = Rentable({ 98 | isListed: _isListed, 99 | pricePerDay: _pricePerDay, 100 | minDays: _minDays, 101 | maxDays: _maxDays 102 | }); 103 | 104 | emit SetRentable( 105 | _nft, 106 | _tokenId, 107 | rentables[_nft][_tokenId] 108 | ); 109 | } 110 | 111 | function getRentable( 112 | address _nft, 113 | uint256 _tokenId 114 | ) external view returns(Rentable memory) { 115 | return rentables[_nft][_tokenId]; 116 | } 117 | 118 | function getRentables( 119 | address _nft, 120 | uint256[] calldata _tokenIds 121 | ) external view returns(Rentable[] memory) { 122 | uint256 length = _tokenIds.length; 123 | Rentable[] memory returnRentables = new Rentable[](length); 124 | for (uint i = 0; i < length; i++) { 125 | returnRentables[i] = rentables[_nft][_tokenIds[i]]; 126 | } 127 | return returnRentables; 128 | } 129 | 130 | function setListed( 131 | address _nft, 132 | uint256 _tokenId, 133 | bool _isListed 134 | ) external { 135 | _setListed(_nft, _tokenId, _isListed); 136 | } 137 | 138 | function setListedBatch( 139 | address _nft, 140 | uint256[] calldata _tokenIds, 141 | bool _isListed 142 | ) external { 143 | uint256 length = _tokenIds.length; 144 | for (uint i = 0; i < length; i++) { 145 | _setListed(_nft, _tokenIds[i], _isListed); 146 | } 147 | } 148 | 149 | function _setListed( 150 | address _nft, 151 | uint256 _tokenId, 152 | bool _isListed 153 | ) private { 154 | address tokenOwner = IERC4907A(_nft).ownerOf(_tokenId); 155 | require(_checkApproved(_nft, _tokenId, tokenOwner), 'Token is not approved for rentals'); 156 | require(tokenOwner == msg.sender, 'Must be token owner to set listed'); 157 | Rentable storage rentable = rentables[_nft][_tokenId]; 158 | rentable.isListed = _isListed; 159 | 160 | emit SetRentable( 161 | _nft, 162 | _tokenId, 163 | rentable 164 | ); 165 | } 166 | 167 | function rent( 168 | address _nft, 169 | uint256 _tokenId, 170 | uint16 _days 171 | ) external payable { 172 | Rentable memory rentable = rentables[_nft][_tokenId]; 173 | 174 | // get the current token owner 175 | address tokenOwner = IERC4907A(_nft).ownerOf(_tokenId); 176 | 177 | // check for token approval or operator approval 178 | require(_checkApproved(_nft, _tokenId, tokenOwner), 'Token is not approved for rentals'); 179 | 180 | // check for active listing 181 | require(rentable.isListed, 'Rentals are not active for this token'); 182 | 183 | // check for current renter 184 | require(IERC4907A(_nft).userOf(_tokenId) == address(0), 'Token is already rented'); 185 | 186 | // check for valid rental duration 187 | require((_days >= rentable.minDays) && (_days <= rentable.maxDays), 'Invalid rental duration'); 188 | 189 | // check for rental fee 190 | uint256 pricePerDay = rentables[_nft][_tokenId].pricePerDay; 191 | require(pricePerDay > 0, 'Rental fee has not been set'); 192 | 193 | // check for rental fee payment 194 | uint256 rentalPrice = pricePerDay * _days; 195 | uint256 payment = msg.value; 196 | require(payment >= rentalPrice, 'Not enough funds sent for rental'); 197 | 198 | // check for and pay royalty 199 | if ( IERC4907A(_nft).supportsInterface(0x2a55205a) ) { 200 | address royaltyRecipient; 201 | uint256 royaltyAmount; 202 | (royaltyRecipient, royaltyAmount) = IERC2981(_nft).royaltyInfo(_tokenId, rentalPrice); 203 | 204 | if ( royaltyRecipient != address(0) && royaltyAmount > 0 ) { 205 | (bool royaltyPaid, ) = payable(royaltyRecipient).call{ value: royaltyAmount }(""); 206 | require(royaltyPaid, "Failed to send royalty to royalty recipient"); 207 | payment -= royaltyAmount; 208 | } 209 | } 210 | 211 | // pay rental fee to token owner 212 | (bool rentalFeePaid, ) = payable(tokenOwner).call{ value: payment }(""); 213 | require(rentalFeePaid, "Failed to send rental fee to token owner"); 214 | 215 | // set renter as user for rental period 216 | uint64 expires = uint64(block.timestamp + (_days * 1 days)); 217 | IERC4907A(_nft).setUser(_tokenId, msg.sender, expires); 218 | 219 | emit Rent( 220 | _nft, 221 | _tokenId, 222 | msg.sender, 223 | expires 224 | ); 225 | } 226 | 227 | function _checkApproved( 228 | address _nft, 229 | uint256 _tokenId, 230 | address _tokenOwner 231 | ) private view returns (bool) { 232 | return IERC4907A(_nft).getApproved(_tokenId) == address(this) 233 | || IERC4907A(_nft).isApprovedForAll(_tokenOwner, address(this)); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /contracts/DCNTSDK.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | 18 | import "@openzeppelin/contracts/access/Ownable.sol"; 19 | import "@openzeppelin/contracts/proxy/Clones.sol"; 20 | 21 | import "./interfaces/IDCNTRegistry.sol"; 22 | import "./interfaces/IDCNTSeries.sol"; 23 | import "./storage/EditionConfig.sol"; 24 | import "./storage/MetadataConfig.sol"; 25 | import "./storage/TokenGateConfig.sol"; 26 | import "./storage/CrescendoConfig.sol"; 27 | 28 | contract DCNTSDK is Ownable { 29 | /// ============ Storage =========== 30 | /// @notice implementation addresses for base contracts 31 | address public DCNT721AImplementation; 32 | address public DCNT4907AImplementation; 33 | address public DCNTSeriesImplementation; 34 | address public DCNTCrescendoImplementation; 35 | address public DCNTVaultImplementation; 36 | address public DCNTStakingImplementation; 37 | address public ZKEditionImplementation; 38 | 39 | /// @notice address of the metadata renderer 40 | address public metadataRenderer; 41 | 42 | /// @notice address of the associated registry 43 | address public contractRegistry; 44 | 45 | /// ============ Events ============ 46 | 47 | /// @notice Emitted after successfully deploying a contract 48 | event DeployDCNT721A(address DCNT721A); 49 | event DeployDCNT4907A(address DCNT4907A); 50 | event DeployDCNTSeries(address DCNTSeries); 51 | event DeployDCNTCrescendo(address DCNTCrescendo); 52 | event DeployDCNTVault(address DCNTVault); 53 | event DeployDCNTStaking(address DCNTStaking); 54 | event DeployZKEdition(address ZKEdition); 55 | 56 | /// ============ Constructor ============ 57 | 58 | /// @notice Creates a new DecentSDK instance 59 | constructor( 60 | address _DCNT721AImplementation, 61 | address _DCNT4907AImplementation, 62 | address _DCNTSeriesImplementation, 63 | address _DCNTCrescendoImplementation, 64 | address _DCNTVaultImplementation, 65 | address _DCNTStakingImplementation, 66 | address _metadataRenderer, 67 | address _contractRegistry, 68 | address _ZKEditionImplementation 69 | ) { 70 | DCNT721AImplementation = _DCNT721AImplementation; 71 | DCNT4907AImplementation = _DCNT4907AImplementation; 72 | DCNTSeriesImplementation = _DCNTSeriesImplementation; 73 | DCNTCrescendoImplementation = _DCNTCrescendoImplementation; 74 | DCNTVaultImplementation = _DCNTVaultImplementation; 75 | DCNTStakingImplementation = _DCNTStakingImplementation; 76 | metadataRenderer = _metadataRenderer; 77 | contractRegistry = _contractRegistry; 78 | ZKEditionImplementation = _ZKEditionImplementation; 79 | } 80 | 81 | /// ============ Functions ============ 82 | 83 | /// @notice deploy and initialize an erc721a clone 84 | function deployDCNT721A( 85 | EditionConfig calldata _editionConfig, 86 | MetadataConfig calldata _metadataConfig, 87 | TokenGateConfig calldata _tokenGateConfig 88 | ) external returns (address clone) { 89 | clone = Clones.clone(DCNT721AImplementation); 90 | (bool success, ) = clone.call( 91 | abi.encodeWithSignature( 92 | "initialize(" 93 | "address," 94 | "(string,string,bool,bool,uint32,uint32,uint32,uint32,uint32,uint32,uint16,uint96,address,address,bytes32)," 95 | "(string,string,bytes,address)," 96 | "(address,uint88,uint8)," 97 | "address" 98 | ")", 99 | msg.sender, 100 | _editionConfig, 101 | _metadataConfig, 102 | _tokenGateConfig, 103 | metadataRenderer 104 | ) 105 | ); 106 | require(success); 107 | IDCNTRegistry(contractRegistry).register(msg.sender, clone, "DCNT721A"); 108 | emit DeployDCNT721A(clone); 109 | } 110 | 111 | /// @notice deploy and initialize a ZKEdition clone 112 | function deployZKEdition( 113 | EditionConfig calldata _editionConfig, 114 | MetadataConfig calldata _metadataConfig, 115 | TokenGateConfig calldata _tokenGateConfig, 116 | address zkVerifier 117 | ) external returns (address clone) { 118 | clone = Clones.clone(ZKEditionImplementation); //zkedition implementation 119 | (bool success, ) = clone.call( 120 | abi.encodeWithSignature( 121 | "initialize(" 122 | "address," 123 | "(string,string,bool,bool,uint32,uint32,uint32,uint32,uint32,uint32,uint16,uint96,address,address,bytes32)," 124 | "(string,string,bytes,address)," 125 | "(address,uint88,uint8)," 126 | "address," 127 | "address" 128 | ")", 129 | msg.sender, 130 | _editionConfig, 131 | _metadataConfig, 132 | _tokenGateConfig, 133 | metadataRenderer, 134 | zkVerifier 135 | ) 136 | ); 137 | require(success); 138 | IDCNTRegistry(contractRegistry).register(msg.sender, clone, "ZKEdition"); 139 | emit DeployZKEdition(clone); 140 | } 141 | 142 | /// @notice deploy and initialize an erc4907a clone 143 | function deployDCNT4907A( 144 | EditionConfig calldata _editionConfig, 145 | MetadataConfig calldata _metadataConfig, 146 | TokenGateConfig calldata _tokenGateConfig 147 | ) external returns (address clone) { 148 | clone = Clones.clone(DCNT4907AImplementation); 149 | (bool success, ) = clone.call( 150 | abi.encodeWithSignature( 151 | "initialize(" 152 | "address," 153 | "(string,string,bool,bool,uint32,uint32,uint32,uint32,uint32,uint32,uint16,uint96,address,address,bytes32)," 154 | "(string,string,bytes,address)," 155 | "(address,uint88,uint8)," 156 | "address" 157 | ")", 158 | msg.sender, 159 | _editionConfig, 160 | _metadataConfig, 161 | _tokenGateConfig, 162 | metadataRenderer 163 | ) 164 | ); 165 | require(success); 166 | IDCNTRegistry(contractRegistry).register(msg.sender, clone, "DCNT4907A"); 167 | emit DeployDCNT4907A(clone); 168 | } 169 | 170 | // deploy and initialize an erc1155 clone 171 | function deployDCNTSeries( 172 | IDCNTSeries.SeriesConfig calldata _config, 173 | IDCNTSeries.Drop calldata _defaultDrop, 174 | IDCNTSeries.DropMap calldata _dropOverrides 175 | ) external returns (address clone) { 176 | clone = Clones.clone(DCNTSeriesImplementation); 177 | (bool success, ) = clone.call( 178 | abi.encodeWithSignature( 179 | "initialize(" 180 | "address," 181 | "(string,string,string,string,uint128,uint128,uint16,address,address,address,bool,bool)," 182 | "(uint32,uint32,uint32,uint32,uint32,uint32,uint96,bytes32,(address,uint88,uint8))," 183 | "(" 184 | "uint256[]," 185 | "uint256[]," 186 | "uint256[]," 187 | "(uint32,uint32,uint32,uint32,uint32,uint32,uint96,bytes32,(address,uint88,uint8))[]" 188 | ")" 189 | ")", 190 | msg.sender, 191 | _config, 192 | _defaultDrop, 193 | _dropOverrides 194 | ) 195 | ); 196 | require(success); 197 | IDCNTRegistry(contractRegistry).register( 198 | msg.sender, 199 | clone, 200 | "DCNTSeries" 201 | ); 202 | emit DeployDCNTSeries(clone); 203 | } 204 | 205 | // deploy and initialize a Crescendo clone 206 | function deployDCNTCrescendo( 207 | CrescendoConfig calldata _config, 208 | MetadataConfig calldata _metadataConfig 209 | ) external returns (address clone) { 210 | clone = Clones.clone(DCNTCrescendoImplementation); 211 | (bool success, ) = clone.call( 212 | abi.encodeWithSignature( 213 | "initialize(" 214 | "address," 215 | "(string,string,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)," 216 | "(string,string,bytes,address)," 217 | "address" 218 | ")", 219 | msg.sender, 220 | _config, 221 | _metadataConfig, 222 | metadataRenderer 223 | ) 224 | ); 225 | require(success); 226 | IDCNTRegistry(contractRegistry).register( 227 | msg.sender, 228 | clone, 229 | "DCNTCrescendo" 230 | ); 231 | emit DeployDCNTCrescendo(clone); 232 | } 233 | 234 | // deploy and initialize a vault wrapper clone 235 | function deployDCNTVault( 236 | address _vaultDistributionTokenAddress, 237 | address _nftVaultKeyAddress, 238 | uint256 _nftTotalSupply, 239 | uint256 _unlockDate 240 | ) external returns (address clone) { 241 | clone = Clones.clone(DCNTVaultImplementation); 242 | (bool success, ) = clone.call( 243 | abi.encodeWithSignature( 244 | "initialize(address,address,address,uint256,uint256)", 245 | msg.sender, 246 | _vaultDistributionTokenAddress, 247 | _nftVaultKeyAddress, 248 | _nftTotalSupply, 249 | _unlockDate 250 | ) 251 | ); 252 | require(success); 253 | IDCNTRegistry(contractRegistry).register(msg.sender, clone, "DCNTVault"); 254 | emit DeployDCNTVault(clone); 255 | } 256 | 257 | // deploy and initialize a vault wrapper clone 258 | function deployDCNTStaking( 259 | address _nft, 260 | address _token, 261 | uint256 _vaultDuration, 262 | uint256 _totalSupply 263 | ) external returns (address clone) { 264 | clone = Clones.clone(DCNTStakingImplementation); 265 | (bool success, ) = clone.call( 266 | abi.encodeWithSignature( 267 | "initialize(address,address,address,uint256,uint256)", 268 | msg.sender, 269 | _nft, 270 | _token, 271 | _vaultDuration, 272 | _totalSupply 273 | ) 274 | ); 275 | require(success); 276 | IDCNTRegistry(contractRegistry).register(msg.sender, clone, "DCNTStaking"); 277 | emit DeployDCNTStaking(clone); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /contracts/DCNTStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT LICENSE 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | 18 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 19 | import "@openzeppelin/contracts/access/Ownable.sol"; 20 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 21 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 22 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 23 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 24 | 25 | contract DCNTStaking is 26 | Initializable, 27 | Ownable, 28 | ReentrancyGuard, 29 | IERC721Receiver 30 | { 31 | uint256 public totalStaked; 32 | 33 | // struct to store a stake's token, owner, and earning values 34 | struct Stake { 35 | uint24 tokenId; 36 | uint48 timestamp; 37 | address owner; 38 | } 39 | 40 | event NFTStaked(address owner, uint256 tokenId, uint256 value); 41 | event NFTUnstaked(address owner, uint256 tokenId, uint256 value); 42 | event Claimed(address owner, uint256 amount); 43 | 44 | address public nftAddress; 45 | address public erc20Address; 46 | uint256 public vaultStart; 47 | uint256 public vaultEnd; 48 | uint256 public totalClaimed; 49 | uint256 public totalSupply; 50 | 51 | // maps tokenId to stake 52 | mapping(uint256 => Stake) public vault; 53 | 54 | function initialize( 55 | address _owner, 56 | address _nft, 57 | address _token, 58 | uint256 _vaultDuration, 59 | uint256 _totalSupply 60 | ) public initializer { 61 | _transferOwnership(_owner); 62 | nftAddress = _nft; 63 | erc20Address = _token; 64 | vaultStart = block.timestamp; 65 | vaultEnd = vaultStart + (_vaultDuration * 1 days); 66 | totalSupply = _totalSupply; 67 | } 68 | 69 | function stake(uint256[] calldata tokenIds) external nonReentrant { 70 | uint256 tokenId; 71 | totalStaked += tokenIds.length; 72 | for (uint256 i; i != tokenIds.length; i++) { 73 | tokenId = tokenIds[i]; 74 | require(vault[tokenId].owner == address(0), "already staked"); 75 | require( 76 | IERC721(nftAddress).ownerOf(tokenId) == msg.sender, 77 | "not your token" 78 | ); 79 | require( 80 | IERC721(nftAddress).getApproved(tokenId) == address(this) 81 | || IERC721(nftAddress).isApprovedForAll(msg.sender, address(this)), 82 | "not approved for transfer" 83 | ); 84 | 85 | IERC721(nftAddress).safeTransferFrom(msg.sender, address(this), tokenId); 86 | emit NFTStaked(msg.sender, tokenId, block.timestamp); 87 | 88 | vault[tokenId] = Stake({ 89 | owner: msg.sender, 90 | tokenId: uint24(tokenId), 91 | timestamp: uint48(min(block.timestamp, vaultEnd)) 92 | }); 93 | } 94 | } 95 | 96 | function _unstakeMany(address account, uint256[] calldata tokenIds) internal { 97 | uint256 tokenId; 98 | totalStaked -= tokenIds.length; 99 | for (uint256 i; i != tokenIds.length; i++) { 100 | tokenId = tokenIds[i]; 101 | Stake memory staked = vault[tokenId]; 102 | require(staked.owner == msg.sender, "not an owner"); 103 | 104 | delete vault[tokenId]; 105 | emit NFTUnstaked(account, tokenId, block.timestamp); 106 | IERC721(nftAddress).safeTransferFrom(address(this), account, tokenId); 107 | } 108 | } 109 | 110 | function claim(uint256[] calldata tokenIds) external nonReentrant { 111 | _claim(msg.sender, tokenIds, false); 112 | } 113 | 114 | function claimForAddress(address account, uint256[] calldata tokenIds) 115 | external 116 | nonReentrant 117 | { 118 | _claim(account, tokenIds, false); 119 | } 120 | 121 | function unstake(uint256[] calldata tokenIds) external nonReentrant { 122 | _claim(msg.sender, tokenIds, true); 123 | } 124 | 125 | function _claim( 126 | address account, 127 | uint256[] calldata tokenIds, 128 | bool _unstake 129 | ) internal { 130 | uint256 tokenId; 131 | uint256 earned = 0; 132 | 133 | for (uint256 i; i != tokenIds.length; i++) { 134 | tokenId = tokenIds[i]; 135 | Stake memory staked = vault[tokenId]; 136 | require(staked.owner == account, "not an owner"); 137 | uint256 stakedAt = staked.timestamp; 138 | uint256 currentTime = min(block.timestamp, vaultEnd); 139 | 140 | earned += calculateEarn(stakedAt); 141 | 142 | vault[tokenId] = Stake({ 143 | owner: account, 144 | tokenId: uint24(tokenId), 145 | timestamp: uint48(currentTime) 146 | }); 147 | } 148 | if (earned > 0) { 149 | IERC20(erc20Address).transfer(account, earned); 150 | totalClaimed += earned; 151 | } 152 | if (_unstake) { 153 | _unstakeMany(account, tokenIds); 154 | } 155 | emit Claimed(account, earned); 156 | } 157 | 158 | function calculateEarn(uint256 stakedAt) internal view returns (uint256) { 159 | uint256 vaultBalance = IERC20(erc20Address).balanceOf(address(this)); 160 | uint256 totalFunding = vaultBalance + totalClaimed; 161 | 162 | uint256 vaultDuration = vaultEnd - vaultStart; 163 | uint256 vaultDays = vaultDuration / 1 days; 164 | 165 | uint256 payout = totalFunding / totalSupply / vaultDays; 166 | uint256 stakeDuration = min(block.timestamp, vaultEnd) - stakedAt; 167 | 168 | return (payout * stakeDuration) / 1 days; 169 | } 170 | 171 | function earningInfo(address account, uint256[] calldata tokenIds) 172 | external 173 | view 174 | returns (uint256) 175 | { 176 | uint256 tokenId; 177 | uint256 earned = 0; 178 | 179 | for (uint256 i; i != tokenIds.length; i++) { 180 | tokenId = tokenIds[i]; 181 | Stake memory staked = vault[tokenId]; 182 | require(staked.owner == account, "not an owner"); 183 | uint256 stakedAt = staked.timestamp; 184 | earned += calculateEarn(stakedAt); 185 | } 186 | return earned; 187 | } 188 | 189 | // get number of tokens staked in account 190 | function balanceOf(address account) external view returns (uint256) { 191 | uint256 balance = 0; 192 | 193 | for (uint256 i = 0; i <= totalSupply; i++) { 194 | if (vault[i].owner == account) { 195 | balance++; 196 | } 197 | } 198 | return balance; 199 | } 200 | 201 | // return nft tokens staked of owner 202 | function tokensOfOwner(address account) 203 | external 204 | view 205 | returns (uint256[] memory ownerTokens) 206 | { 207 | uint256[] memory tmp = new uint256[](totalSupply); 208 | 209 | uint256 index = 0; 210 | for (uint256 tokenId = 0; tokenId <= totalSupply; tokenId++) { 211 | if (vault[tokenId].owner == account) { 212 | tmp[index] = vault[tokenId].tokenId; 213 | index++; 214 | } 215 | } 216 | 217 | uint256[] memory tokens = new uint256[](index); 218 | for (uint256 i; i != index; i++) { 219 | tokens[i] = tmp[i]; 220 | } 221 | 222 | return tokens; 223 | } 224 | 225 | function min(uint256 a, uint256 b) internal pure returns (uint256) { 226 | return a >= b ? b : a; 227 | } 228 | 229 | function onERC721Received( 230 | address, 231 | address, 232 | // address from, 233 | uint256, 234 | bytes calldata 235 | ) external pure override returns (bytes4) { 236 | // require(from == address(0x0), "Cannot send nfts to Vault directly"); 237 | return IERC721Receiver.onERC721Received.selector; 238 | } 239 | 240 | function withdraw(uint256 amount) external onlyOwner { 241 | IERC20(erc20Address).transfer(address(this), amount); 242 | } 243 | 244 | // fallback 245 | fallback() external payable {} 246 | 247 | // receive eth 248 | receive() external payable {} 249 | } 250 | -------------------------------------------------------------------------------- /contracts/DCNTVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | 18 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 19 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 20 | import "@openzeppelin/contracts/access/Ownable.sol"; 21 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 22 | 23 | /// @title Decentralized Creator Nonfungible Token Vault Wrapper (DCNT VWs) 24 | /// @notice claimable ERC20s for NFT holders after vault expiration 25 | contract DCNTVault is Ownable, Initializable { 26 | /// ============ Immutable storage ============ 27 | 28 | /// ============ Mutable storage ============ 29 | 30 | /// @notice vault token to be distributed to token holders 31 | IERC20 public vaultDistributionToken; 32 | /// @notice "ticket" token held by user 33 | IERC721 public nftVaultKey; 34 | /// @notice total supply of nft used in determining payouts 35 | uint256 public nftTotalSupply; 36 | /// @notice unlock date when distribution can start happening 37 | uint256 public unlockDate; 38 | 39 | /// @notice Mapping of addresses who have claimed tokens 40 | mapping(uint256 => bool) internal hasClaimedTokenId; 41 | 42 | /// @notice total # of tokens already released 43 | uint256 private _totalReleased; 44 | 45 | /// ============ Events ============ 46 | 47 | /// @notice Emitted after a successful token claim 48 | /// @param account recipient of claim 49 | /// @param amount of tokens claimed 50 | event Claimed(address account, uint256 amount); 51 | 52 | /// ============ Initializer ============ 53 | 54 | /// @notice Initializes a new vault 55 | /// @param _vaultDistributionTokenAddress of token 56 | /// @param _nftVaultKeyAddress of token 57 | /// @param _unlockDate date of vault expiration 58 | function initialize( 59 | address _owner, 60 | address _vaultDistributionTokenAddress, 61 | address _nftVaultKeyAddress, 62 | uint256 _nftTotalSupply, 63 | uint256 _unlockDate 64 | ) public initializer { 65 | _transferOwnership(_owner); 66 | vaultDistributionToken = IERC20(_vaultDistributionTokenAddress); 67 | nftVaultKey = IERC721(_nftVaultKeyAddress); 68 | nftTotalSupply = _nftTotalSupply; 69 | unlockDate = _unlockDate; 70 | } 71 | 72 | /// ============ Functions ============ 73 | 74 | // returns balance of vault 75 | function vaultBalance() public view returns (uint256) { 76 | return vaultDistributionToken.balanceOf(address(this)); 77 | } 78 | 79 | // returns total # of tokens already released from vault 80 | function totalReleased() public view returns (uint256) { 81 | return _totalReleased; 82 | } 83 | 84 | // (total vault balance) * (nfts_owned/total_nfts) 85 | function _pendingPayment(uint256 numNftVaultKeys, uint256 totalReceived) 86 | private 87 | view 88 | returns (uint256) 89 | { 90 | return (totalReceived * numNftVaultKeys) / nftTotalSupply; 91 | } 92 | 93 | function _claimMany(address to, uint256[] memory tokenIds) private { 94 | require(block.timestamp >= unlockDate, "vault is still locked"); 95 | require(vaultBalance() > 0, "vault is empty"); 96 | for (uint256 i = 0; i < tokenIds.length; i++) { 97 | require( 98 | nftVaultKey.ownerOf(tokenIds[i]) == to, 99 | "address does not own token" 100 | ); 101 | require(!hasClaimedTokenId[tokenIds[i]], "token already claimed"); 102 | hasClaimedTokenId[tokenIds[i]] = true; 103 | } 104 | 105 | uint256 amount = _pendingPayment( 106 | tokenIds.length, 107 | vaultBalance() + totalReleased() 108 | ); 109 | require(amount > 0, "address has no claimable tokens"); 110 | require(vaultDistributionToken.transfer(to, amount), "Transfer failed"); 111 | _totalReleased += amount; 112 | emit Claimed(to, amount); 113 | } 114 | 115 | // claim tokens for multiple NFTs in collection 116 | function claimMany(address to, uint256[] calldata tokenIds) external { 117 | _claimMany(to, tokenIds); 118 | } 119 | 120 | // serves similar purpose to claim all but allows user to claim specific 121 | // token for one of NFTs in collection 122 | function claim(address to, uint256 tokenId) external { 123 | _claimMany(to, _asSingletonArray(tokenId)); 124 | } 125 | 126 | // allows vault owner to claim ERC20 tokens sent to account 127 | // failsafe in case money needs to be taken off chain 128 | function drain(IERC20 token) public onlyOwner { 129 | token.transfer(msg.sender, token.balanceOf(address(this))); 130 | } 131 | 132 | function drainEth() public onlyOwner { 133 | (bool success, ) = payable(msg.sender).call{value: address(this).balance}(""); 134 | require(success, "Could not drain ETH"); 135 | } 136 | 137 | function _asSingletonArray(uint256 element) 138 | private 139 | pure 140 | returns (uint256[] memory) 141 | { 142 | uint256[] memory array = new uint256[](1); 143 | array[0] = element; 144 | 145 | return array; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /contracts/DCNTVaultNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | 18 | import "@openzeppelin/contracts/access/Ownable.sol"; 19 | 20 | import "./interfaces/IDCNTSDK.sol"; 21 | import "./storage/EditionConfig.sol"; 22 | import "./storage/MetadataConfig.sol"; 23 | import "./storage/TokenGateConfig.sol"; 24 | 25 | contract DCNTVaultNFT is Ownable { 26 | /// ============ Immutable storage ============ 27 | 28 | /// @notice implementation addresses for base contracts 29 | address public DCNT721AImplementation; 30 | address public DCNT4907AImplementation; 31 | address public DCNTSeriesImplementation; 32 | address public DCNTCrescendoImplementation; 33 | address public DCNTVaultImplementation; 34 | address public DCNTStakingImplementation; 35 | address public ZKEditionImplementation; 36 | 37 | /// @notice address of the metadata renderer 38 | address public metadataRenderer; 39 | 40 | /// @notice address of the associated registry 41 | address public contractRegistry; 42 | 43 | /// ============ Events ============ 44 | 45 | /// @notice Emitted after successfully deploying a contract 46 | event Create(address nft, address vault); 47 | 48 | /// ============ Constructor ============ 49 | 50 | /// @notice Creates a new DecentVaultWrapped instance 51 | constructor(address _DCNTSDK) { 52 | IDCNTSDK sdk = IDCNTSDK(_DCNTSDK); 53 | DCNT721AImplementation = sdk.DCNT721AImplementation(); 54 | DCNT4907AImplementation = sdk.DCNT4907AImplementation(); 55 | DCNTVaultImplementation = sdk.DCNTVaultImplementation(); 56 | metadataRenderer = sdk.metadataRenderer(); 57 | contractRegistry = sdk.contractRegistry(); 58 | } 59 | 60 | /// ============ Functions ============ 61 | 62 | function create( 63 | address _DCNTSDK, 64 | EditionConfig memory _editionConfig, 65 | MetadataConfig memory _metadataConfig, 66 | TokenGateConfig memory _tokenGateConfig, 67 | address _vaultDistributionTokenAddress, 68 | uint256 _unlockDate, 69 | bool _supports4907 70 | ) external returns (address nft, address vault) { 71 | address deployedNFT; 72 | if (_supports4907) { 73 | (bool success1, bytes memory data1) = _DCNTSDK.delegatecall( 74 | abi.encodeWithSignature( 75 | "deployDCNT4907A(" 76 | "(string,string,bool,bool,uint32,uint32,uint32,uint32,uint32,uint32,uint16,uint96,address,address,bytes32)," 77 | "(string,string,bytes,address)," 78 | "(address,uint88,uint8)" 79 | ")", 80 | _editionConfig, 81 | _metadataConfig, 82 | _tokenGateConfig 83 | ) 84 | ); 85 | 86 | require(success1); 87 | deployedNFT = abi.decode(data1, (address)); 88 | } else { 89 | (bool success2, bytes memory data2) = _DCNTSDK.delegatecall( 90 | abi.encodeWithSignature( 91 | "deployDCNT721A(" 92 | "(string,string,bool,bool,uint32,uint32,uint32,uint32,uint32,uint32,uint16,uint96,address,address,bytes32)," 93 | "(string,string,bytes,address)," 94 | "(address,uint88,uint8)" 95 | ")", 96 | _editionConfig, 97 | _metadataConfig, 98 | _tokenGateConfig 99 | ) 100 | ); 101 | 102 | require(success2); 103 | deployedNFT = abi.decode(data2, (address)); 104 | } 105 | 106 | (bool success, bytes memory data) = _DCNTSDK.delegatecall( 107 | abi.encodeWithSignature( 108 | "deployDCNTVault(address,address,uint256,uint256)", 109 | _vaultDistributionTokenAddress, 110 | deployedNFT, 111 | _editionConfig.maxTokens, 112 | _unlockDate 113 | ) 114 | ); 115 | 116 | require(success); 117 | address deployedVault = abi.decode(data, (address)); 118 | 119 | emit Create(deployedNFT, deployedVault); 120 | return (deployedNFT, deployedVault); 121 | } 122 | 123 | function bytesToAddress(bytes memory _bytes) 124 | private 125 | pure 126 | returns (address addr) 127 | { 128 | assembly { 129 | addr := mload(add(_bytes, 32)) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /contracts/FeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import '@openzeppelin/contracts/access/Ownable.sol'; 5 | 6 | import './interfaces/IFeeManager.sol'; 7 | import './utils/Splits.sol'; 8 | import './utils/Version.sol'; 9 | 10 | /// @title template NFT contract 11 | contract FeeManager is IFeeManager, Ownable, Splits, Version(1) { 12 | 13 | uint256 public fee; 14 | uint256 public commissionBPS; 15 | 16 | constructor(uint256 _fee, uint256 _commissionBPS) { 17 | fee = _fee; 18 | commissionBPS = _commissionBPS; 19 | } 20 | 21 | function setFees(uint256 _fee, uint256 _commissionBPS) external onlyOwner { 22 | fee = _fee; 23 | commissionBPS = _commissionBPS; 24 | } 25 | 26 | function _calculateFee(uint256 /* salePrice */, uint256 quantity) private view returns (uint256) { 27 | return fee == 0 ? 0 : fee * quantity; 28 | } 29 | 30 | function _calculateCommission(uint256 salePrice, uint256 quantity) private view returns (uint256) { 31 | return commissionBPS == 0 ? 0 : (salePrice * commissionBPS / 100_00) * quantity; 32 | } 33 | 34 | function calculateFees(uint256 salePrice, uint256 quantity) external view returns (uint256 fees, uint256 commission) { 35 | return ( 36 | _calculateFee(salePrice, quantity), 37 | _calculateCommission(salePrice, quantity) 38 | ); 39 | } 40 | 41 | function recipient() external view returns (address) { 42 | return address(this); 43 | } 44 | 45 | function withdraw() external onlyOwner { 46 | if ( splitWallet != address(0) ) { 47 | revert SplitsAreActive(); 48 | } 49 | (bool success, ) = payable(msg.sender).call{value: address(this).balance}(""); 50 | if ( ! success ) { 51 | revert WithdrawFailed(); 52 | } 53 | } 54 | 55 | receive() external payable { } 56 | } 57 | -------------------------------------------------------------------------------- /contracts/ZKEdition.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | 18 | import "./DCNT721A.sol"; 19 | 20 | /// @title template NFT contract 21 | contract ZKEdition is DCNT721A { 22 | 23 | address public zkVerifier; 24 | 25 | /// ============ Constructor ============ 26 | 27 | function initialize( 28 | address _owner, 29 | EditionConfig memory _editionConfig, 30 | MetadataConfig memory _metadataConfig, 31 | TokenGateConfig memory _tokenGateConfig, 32 | address _metadataRenderer, 33 | address _zkVerifier 34 | ) public initializer { 35 | _transferOwnership(_owner); 36 | _grantRole(DEFAULT_ADMIN_ROLE, _owner); 37 | _name = _editionConfig.name; 38 | _symbol = _editionConfig.symbol; 39 | _currentIndex = _startTokenId(); 40 | 41 | parentIP = _metadataConfig.parentIP; 42 | tokenGateConfig = _tokenGateConfig; 43 | zkVerifier = _zkVerifier; 44 | 45 | edition = Edition({ 46 | hasAdjustableCap: _editionConfig.hasAdjustableCap, 47 | isSoulbound: _editionConfig.isSoulbound, 48 | maxTokens: _editionConfig.maxTokens, 49 | tokenPrice: _editionConfig.tokenPrice, 50 | maxTokenPurchase: _editionConfig.maxTokenPurchase, 51 | presaleMerkleRoot: _editionConfig.presaleMerkleRoot, 52 | presaleStart: _editionConfig.presaleStart, 53 | presaleEnd: _editionConfig.presaleEnd, 54 | saleStart: _editionConfig.saleStart, 55 | saleEnd: _editionConfig.saleEnd, 56 | royaltyBPS: _editionConfig.royaltyBPS, 57 | payoutAddress: _editionConfig.payoutAddress 58 | }); 59 | 60 | if ( 61 | _metadataRenderer != address(0) && 62 | _metadataConfig.metadataRendererInit.length > 0 63 | ) { 64 | metadataRenderer = _metadataRenderer; 65 | IMetadataRenderer(_metadataRenderer).initializeWithData( 66 | _metadataConfig.metadataRendererInit 67 | ); 68 | } else { 69 | _contractURI = _metadataConfig.contractURI; 70 | baseURI = _metadataConfig.metadataURI; 71 | } 72 | } 73 | 74 | /// @notice allows someone to claim an nft with a valid zk proof 75 | function zkClaim(address recipient) external { 76 | require(msg.sender == zkVerifier, "Only zkVerifier can call"); 77 | uint256 mintIndex = _nextTokenId(); 78 | require( 79 | mintIndex + 1 <= edition.maxTokens, 80 | "Purchase would exceed max supply" 81 | ); 82 | 83 | _safeMint(recipient, 1); 84 | emit Minted(recipient, mintIndex); 85 | } 86 | 87 | function setZKVerifier(address _zkVerifier) external onlyOwner { 88 | zkVerifier = _zkVerifier; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/erc721a/ERC4907A.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // ERC721A Contracts v4.2.2 3 | // Creator: Chiru Labs 4 | 5 | pragma solidity ^0.8.4; 6 | 7 | import "erc721a/contracts/extensions/IERC4907A.sol"; 8 | import "./ERC721A.sol"; 9 | 10 | /** 11 | * @title ERC4907A 12 | * 13 | * @dev [ERC4907](https://eips.ethereum.org/EIPS/eip-4907) compliant 14 | * extension of ERC721A, which allows owners and authorized addresses 15 | * to add a time-limited role with restricted permissions to ERC721 tokens. 16 | */ 17 | abstract contract ERC4907A is ERC721A, IERC4907A { 18 | // The bit position of `expires` in packed user info. 19 | uint256 private constant _BITPOS_EXPIRES = 160; 20 | 21 | // Mapping from token ID to user info. 22 | // 23 | // Bits Layout: 24 | // - [0..159] `user` 25 | // - [160..223] `expires` 26 | mapping(uint256 => uint256) private _packedUserInfo; 27 | 28 | /** 29 | * @dev Sets the `user` and `expires` for `tokenId`. 30 | * The zero address indicates there is no user. 31 | * 32 | * Requirements: 33 | * 34 | * - The caller must own `tokenId` or be an approved operator. 35 | */ 36 | function setUser( 37 | uint256 tokenId, 38 | address user, 39 | uint64 expires 40 | ) public virtual override { 41 | // Require the caller to be either the token owner or an approved operator. 42 | address owner = ownerOf(tokenId); 43 | if (_msgSenderERC721A() != owner) 44 | if (!isApprovedForAll(owner, _msgSenderERC721A())) 45 | if (getApproved(tokenId) != _msgSenderERC721A()) 46 | revert SetUserCallerNotOwnerNorApproved(); 47 | 48 | _packedUserInfo[tokenId] = 49 | (uint256(expires) << _BITPOS_EXPIRES) | 50 | uint256(uint160(user)); 51 | 52 | emit UpdateUser(tokenId, user, expires); 53 | } 54 | 55 | /** 56 | * @dev Returns the user address for `tokenId`. 57 | * The zero address indicates that there is no user or if the user is expired. 58 | */ 59 | function userOf(uint256 tokenId) 60 | public 61 | view 62 | virtual 63 | override 64 | returns (address) 65 | { 66 | uint256 packed = _packedUserInfo[tokenId]; 67 | assembly { 68 | // Branchless `packed *= (block.timestamp <= expires ? 1 : 0)`. 69 | // If the `block.timestamp == expires`, the `lt` clause will be true 70 | // if there is a non-zero user address in the lower 160 bits of `packed`. 71 | packed := mul( 72 | packed, 73 | // `block.timestamp <= expires ? 1 : 0`. 74 | lt(shl(_BITPOS_EXPIRES, timestamp()), packed) 75 | ) 76 | } 77 | return address(uint160(packed)); 78 | } 79 | 80 | /** 81 | * @dev Returns the user's expires of `tokenId`. 82 | */ 83 | function userExpires(uint256 tokenId) 84 | public 85 | view 86 | virtual 87 | override 88 | returns (uint256) 89 | { 90 | return _packedUserInfo[tokenId] >> _BITPOS_EXPIRES; 91 | } 92 | 93 | /** 94 | * @dev Override of {IERC165-supportsInterface}. 95 | */ 96 | function supportsInterface(bytes4 interfaceId) 97 | public 98 | view 99 | virtual 100 | override(ERC721A, IERC721A) 101 | returns (bool) 102 | { 103 | // The interface ID for ERC4907 is `0xad092b5c`, 104 | // as defined in [ERC4907](https://eips.ethereum.org/EIPS/eip-4907). 105 | return super.supportsInterface(interfaceId) || interfaceId == 0xad092b5c; 106 | } 107 | 108 | /** 109 | * @dev Returns the user address for `tokenId`, ignoring the expiry status. 110 | */ 111 | function _explicitUserOf(uint256 tokenId) 112 | internal 113 | view 114 | virtual 115 | returns (address) 116 | { 117 | return address(uint160(_packedUserInfo[tokenId])); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /contracts/extensions/ERC1155Hooks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import 'solmate/src/tokens/ERC1155.sol'; 5 | 6 | abstract contract ERC1155Hooks is ERC1155 { 7 | function _beforeTokenTransfers( 8 | address from, 9 | address to, 10 | uint256[] memory ids, 11 | uint256[] memory amounts 12 | ) internal virtual {} 13 | 14 | function safeTransferFrom( 15 | address from, 16 | address to, 17 | uint256 id, 18 | uint256 amount, 19 | bytes calldata data 20 | ) public virtual override { 21 | _beforeTokenTransfers(from, to, _asSingletonArray(id), _asSingletonArray(amount)); 22 | super.safeTransferFrom(from, to, id, amount, data); 23 | } 24 | 25 | function safeBatchTransferFrom( 26 | address from, 27 | address to, 28 | uint256[] calldata ids, 29 | uint256[] calldata amounts, 30 | bytes calldata data 31 | ) public virtual override { 32 | _beforeTokenTransfers(from, to, ids, amounts); 33 | super.safeBatchTransferFrom(from, to, ids, amounts, data); 34 | } 35 | 36 | function _mint( 37 | address to, 38 | uint256 id, 39 | uint256 amount, 40 | bytes memory data 41 | ) internal virtual override { 42 | _beforeTokenTransfers(address(0), to, _asSingletonArray(id), _asSingletonArray(amount)); 43 | super._mint(to, id, amount, data); 44 | } 45 | 46 | function _batchMint( 47 | address to, 48 | uint256[] memory ids, 49 | uint256[] memory amounts, 50 | bytes memory data 51 | ) internal virtual override { 52 | _beforeTokenTransfers(address(0), to, ids, amounts); 53 | super._batchMint(to, ids, amounts, data); 54 | } 55 | 56 | function _burn( 57 | address from, 58 | uint256 id, 59 | uint256 amount 60 | ) internal virtual override { 61 | _beforeTokenTransfers(msg.sender, address(0), _asSingletonArray(id), _asSingletonArray(amount)); 62 | super._burn(from, id, amount); 63 | } 64 | 65 | function _batchBurn( 66 | address from, 67 | uint256[] memory ids, 68 | uint256[] memory amounts 69 | ) internal virtual override { 70 | _beforeTokenTransfers(msg.sender, address(0), ids, amounts); 71 | super._batchBurn(from, ids, amounts); 72 | } 73 | 74 | function _asSingletonArray(uint256 element) private pure returns (uint256[] memory) { 75 | uint256[] memory array = new uint256[](1); 76 | array[0] = element; 77 | return array; 78 | } 79 | } -------------------------------------------------------------------------------- /contracts/interfaces/IBondingCurve.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IBondingCurve { 5 | event CurvedMint( 6 | address indexed sender, 7 | uint256 amount, 8 | uint256 id, 9 | uint256 deposit 10 | ); 11 | event CurvedBurn( 12 | address indexed sender, 13 | uint256 amount, 14 | uint256 id, 15 | uint256 reimbursement 16 | ); 17 | 18 | function calculateCurvedMintReturn(uint256 amount, uint256 id) 19 | external 20 | view 21 | returns (uint256); 22 | 23 | function calculateCurvedBurnReturn(uint256 amount, uint256 id) 24 | external 25 | view 26 | returns (uint256); 27 | } 28 | -------------------------------------------------------------------------------- /contracts/interfaces/IDCNTRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IDCNTRegistry { 5 | function register( 6 | address _deployer, 7 | address _deployment, 8 | string calldata _key 9 | ) external; 10 | 11 | function remove(address _deployer, address _deployment) external; 12 | 13 | function query(address _deployer) external returns (address[] memory); 14 | } 15 | -------------------------------------------------------------------------------- /contracts/interfaces/IDCNTSDK.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IDCNTSDK { 5 | /// @notice implementation addresses for base contracts 6 | function DCNT721AImplementation() external returns (address); 7 | 8 | function DCNT4907AImplementation() external returns (address); 9 | 10 | function DCNTCrescendoImplementation() external returns (address); 11 | 12 | function DCNTVaultImplementation() external returns (address); 13 | 14 | function DCNTStakingImplementation() external returns (address); 15 | 16 | function metadataRenderer() external returns (address); 17 | 18 | function contractRegistry() external returns (address); 19 | 20 | /// ============ Functions ============ 21 | 22 | // deploy and initialize an erc721a clone 23 | function deployDCNT721A( 24 | string memory _name, 25 | string memory _symbol, 26 | uint256 _maxTokens, 27 | uint256 _tokenPrice, 28 | uint256 _maxTokenPurchase 29 | ) external returns (address clone); 30 | 31 | // deploy and initialize an erc4907a clone 32 | function deployDCNT4907A( 33 | string memory _name, 34 | string memory _symbol, 35 | uint256 _maxTokens, 36 | uint256 _tokenPrice, 37 | uint256 _maxTokenPurchase 38 | ) external returns (address clone); 39 | 40 | // deploy and initialize a Crescendo clone 41 | function deployDCNTCrescendo( 42 | string memory _name, 43 | string memory _symbol, 44 | string memory _uri, 45 | uint256 _initialPrice, 46 | uint256 _step1, 47 | uint256 _step2, 48 | uint256 _hitch, 49 | uint256 _trNum, 50 | uint256 _trDenom, 51 | address payable _payouts 52 | ) external returns (address clone); 53 | 54 | // deploy and initialize a vault wrapper clone 55 | function deployDCNTVault( 56 | address _vaultDistributionTokenAddress, 57 | address _nftVaultKeyAddress, 58 | uint256 _nftTotalSupply, 59 | uint256 _unlockDate 60 | ) external returns (address clone); 61 | 62 | // deploy and initialize a vault wrapper clone 63 | function deployDCNTStaking( 64 | address _nft, 65 | address _token, 66 | uint256 _vaultDuration, 67 | uint256 _totalSupply 68 | ) external returns (address clone); 69 | } 70 | -------------------------------------------------------------------------------- /contracts/interfaces/IFeeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | interface IFeeManager { 5 | 6 | error SplitsAreActive(); 7 | 8 | error WithdrawFailed(); 9 | 10 | function setFees(uint256 _fee, uint256 _commissionBPS) external; 11 | 12 | function calculateFees(uint256 salePrice, uint256 quantity) external view returns (uint256 fee, uint256 commission); 13 | 14 | function recipient() external view returns (address); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /contracts/interfaces/IMetadataRenderer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IMetadataRenderer { 5 | function tokenURI(uint256) external view returns (string memory); 6 | 7 | function contractURI() external view returns (string memory); 8 | 9 | function initializeWithData(bytes memory initData) external; 10 | 11 | /// @notice Storage for token edition information 12 | struct TokenEditionInfo { 13 | string description; 14 | string imageURI; 15 | string animationURI; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/interfaces/IOnChainMetadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IOnChainMetadata { 5 | /// @notice Lyrics updated for this edition 6 | event SongUpdated( 7 | address target, 8 | address sender, 9 | SongMetadata songMetadata, 10 | ProjectMetadata projectMetadata, 11 | string[] tags, 12 | Credit[] credits 13 | ); 14 | 15 | /// @notice AudioQuantitativeUpdated updated for this edition 16 | /// @dev admin function indexer feedback 17 | event AudioQuantitativeUpdated( 18 | address indexed target, 19 | address sender, 20 | string key, 21 | uint256 bpm, 22 | uint256 duration, 23 | string audioMimeType, 24 | uint256 trackNumber 25 | ); 26 | 27 | /// @notice AudioQualitative updated for this edition 28 | /// @dev admin function indexer feedback 29 | event AudioQualitativeUpdated( 30 | address indexed target, 31 | address sender, 32 | string license, 33 | string externalUrl, 34 | string isrc, 35 | string genre 36 | ); 37 | 38 | /// @notice Lyrics updated for this edition 39 | event LyricsUpdated( 40 | address target, 41 | address sender, 42 | string lyrics, 43 | string lyricsNft 44 | ); 45 | 46 | /// @notice Artwork updated for this edition 47 | /// @dev admin function indexer feedback 48 | event ArtworkUpdated( 49 | address indexed target, 50 | address sender, 51 | string artworkUri, 52 | string artworkMimeType, 53 | string artworkNft 54 | ); 55 | 56 | /// @notice Visualizer updated for this edition 57 | /// @dev admin function indexer feedback 58 | event VisualizerUpdated( 59 | address indexed target, 60 | address sender, 61 | string artworkUri, 62 | string artworkMimeType, 63 | string artworkNft 64 | ); 65 | 66 | /// @notice ProjectMetadata updated for this edition 67 | /// @dev admin function indexer feedback 68 | event ProjectArtworkUpdated( 69 | address indexed target, 70 | address sender, 71 | string artworkUri, 72 | string artworkMimeType, 73 | string artworkNft 74 | ); 75 | 76 | /// @notice Tags updated for this edition 77 | /// @dev admin function indexer feedback 78 | event TagsUpdated(address indexed target, address sender, string[] tags); 79 | 80 | /// @notice Credit updated for this edition 81 | /// @dev admin function indexer feedback 82 | event CreditsUpdated( 83 | address indexed target, 84 | address sender, 85 | Credit[] credits 86 | ); 87 | 88 | /// @notice ProjectMetadata updated for this edition 89 | /// @dev admin function indexer feedback 90 | event ProjectPublishingDataUpdated( 91 | address indexed target, 92 | address sender, 93 | string title, 94 | string description, 95 | string recordLabel, 96 | string publisher, 97 | string locationCreated, 98 | string releaseDate, 99 | string projectType, 100 | string upc 101 | ); 102 | 103 | /// @notice PublishingData updated for this edition 104 | /// @dev admin function indexer feedback 105 | event PublishingDataUpdated( 106 | address indexed target, 107 | address sender, 108 | string title, 109 | string description, 110 | string recordLabel, 111 | string publisher, 112 | string locationCreated, 113 | string releaseDate 114 | ); 115 | 116 | /// @notice losslessAudio updated for this edition 117 | /// @dev admin function indexer feedback 118 | event LosslessAudioUpdated( 119 | address indexed target, 120 | address sender, 121 | string losslessAudio 122 | ); 123 | 124 | /// @notice Description updated for this edition 125 | /// @dev admin function indexer feedback 126 | event DescriptionUpdated( 127 | address indexed target, 128 | address sender, 129 | string newDescription 130 | ); 131 | 132 | /// @notice Artist updated for this edition 133 | /// @dev admin function indexer feedback 134 | event ArtistUpdated(address indexed target, address sender, string newArtist); 135 | 136 | /// @notice Event for updated Media URIs 137 | event MediaURIsUpdated( 138 | address indexed target, 139 | address sender, 140 | string imageURI, 141 | string animationURI 142 | ); 143 | 144 | /// @notice Event for a new edition initialized 145 | /// @dev admin function indexer feedback 146 | event EditionInitialized( 147 | address indexed target, 148 | string description, 149 | string imageURI, 150 | string animationURI 151 | ); 152 | 153 | /// @notice Storage for SongMetadata 154 | struct SongMetadata { 155 | SongContent song; 156 | PublishingData songPublishingData; 157 | } 158 | 159 | /// @notice Storage for SongContent 160 | struct SongContent { 161 | Audio audio; 162 | Artwork artwork; 163 | Artwork visualizer; 164 | } 165 | 166 | /// @notice Storage for SongDetails 167 | struct SongDetails { 168 | string artistName; 169 | AudioQuantitative audioQuantitative; 170 | AudioQualitative audioQualitative; 171 | } 172 | 173 | /// @notice Storage for Audio 174 | struct Audio { 175 | string losslessAudio; // ipfs://{cid} or arweave 176 | SongDetails songDetails; 177 | Lyrics lyrics; 178 | } 179 | 180 | /// @notice Storage for AudioQuantitative 181 | struct AudioQuantitative { 182 | string key; // C / A# / etc 183 | uint256 bpm; // 120 / 60 / 100 184 | uint256 duration; // 240 / 60 / 120 185 | string audioMimeType; // audio/wav 186 | uint256 trackNumber; // 1 187 | } 188 | 189 | /// @notice Storage for AudioQualitative 190 | struct AudioQualitative { 191 | string license; // CC0 192 | string externalUrl; // Link to your project website 193 | string isrc; // CC-XXX-YY-NNNNN 194 | string genre; // Rock / Pop / Metal / Hip-Hop / Electronic / Classical / Jazz / Folk / Reggae / Other 195 | } 196 | 197 | /// @notice Storage for Artwork 198 | struct Artwork { 199 | string artworkUri; // The uri of the artwork (ipfs://) 200 | string artworkMimeType; // The mime type of the artwork 201 | string artworkNft; // The NFT of the artwork (caip19) 202 | } 203 | 204 | /// @notice Storage for Lyrics 205 | struct Lyrics { 206 | string lyrics; 207 | string lyricsNft; 208 | } 209 | 210 | /// @notice Storage for PublishingData 211 | struct PublishingData { 212 | string title; 213 | string description; 214 | string recordLabel; // Sony / Universal / etc 215 | string publisher; // Sony / Universal / etc 216 | string locationCreated; 217 | string releaseDate; // 2020-01-01 218 | } 219 | 220 | /// @notice Storage for ProjectMetadata 221 | struct ProjectMetadata { 222 | PublishingData publishingData; 223 | Artwork artwork; 224 | string projectType; // Single / EP / Album 225 | string upc; // 03600029145 226 | } 227 | 228 | /// @notice Storage for Credit 229 | struct Credit { 230 | string name; 231 | string collaboratorType; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /contracts/interfaces/ISharedNFTLogic.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; 18 | import "../interfaces/IOnChainMetadata.sol"; 19 | 20 | /// Shared NFT logic for rendering metadata associated with editions 21 | /// @dev Can safely be used for generic base64Encode and numberToString functions 22 | contract ISharedNFTLogic is IOnChainMetadata { 23 | /// Generate edition metadata from storage information as base64-json blob 24 | /// Combines the media data and metadata 25 | /// @param name the token name 26 | /// @param tokenOfEdition Token ID for specific token 27 | /// @param songMetadata song metadata 28 | /// @param projectMetadata project metadata 29 | /// @param credits The credits of the track 30 | /// @param tags The tags of the track 31 | function createMetadataEdition( 32 | string memory name, 33 | uint256 tokenOfEdition, 34 | SongMetadata memory songMetadata, 35 | ProjectMetadata memory projectMetadata, 36 | Credit[] memory credits, 37 | string[] memory tags 38 | ) external pure returns (string memory) {} 39 | 40 | /// Encodes the argument json bytes into base64-data uri format 41 | /// @param json Raw json to base64 and turn into a data-uri 42 | function encodeMetadataJSON(bytes memory json) 43 | public 44 | pure 45 | returns (string memory) 46 | {} 47 | } 48 | -------------------------------------------------------------------------------- /contracts/interfaces/ITokenWithBalance.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface ITokenWithBalance { 5 | function balanceOf(address owner) external 6 | returns (uint256); 7 | } 8 | -------------------------------------------------------------------------------- /contracts/mocks/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | // based on Uniswaps TestERC20 token 7 | contract MockERC20 is ERC20 { 8 | constructor( 9 | string memory name_, 10 | string memory symbol_, 11 | uint256 amountToMint 12 | ) ERC20(name_, symbol_) { 13 | setBalance(msg.sender, amountToMint); 14 | } 15 | 16 | // sets the balance of the address 17 | // this mints/burns the amount depending on the current balance 18 | function setBalance(address to, uint256 amount) public { 19 | uint256 old = balanceOf(to); 20 | if (old < amount) { 21 | _mint(to, amount - old); 22 | } else if (old > amount) { 23 | _burn(to, old - amount); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/mocks/MockERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /// ============ Imports ============ 5 | 6 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 7 | import "@openzeppelin/contracts/access/Ownable.sol"; 8 | 9 | contract MockERC721 is ERC721, Ownable { 10 | uint256 public totalSupply; 11 | 12 | /// ============ Constructor ============ 13 | 14 | constructor(string memory name, string memory symbol) ERC721(name, symbol) {} 15 | 16 | function mintNft(uint256 numberOfTokens) public payable { 17 | for (uint256 i = 0; i < numberOfTokens; i++) { 18 | _safeMint(msg.sender, totalSupply++); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/mocks/MockV3Aggregator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV2V3Interface.sol"; 5 | 6 | /** 7 | * @title MockV3Aggregator 8 | * @notice Based on the FluxAggregator contract 9 | * @notice Use this contract when you need to test 10 | * other contract's ability to read data from an 11 | * aggregator contract, but how the aggregator got 12 | * its answer is unimportant 13 | */ 14 | contract MockV3Aggregator is AggregatorV2V3Interface { 15 | uint256 public constant override version = 0; 16 | 17 | uint8 public override decimals; 18 | int256 public override latestAnswer; 19 | uint256 public override latestTimestamp; 20 | uint256 public override latestRound; 21 | 22 | mapping(uint256 => int256) public override getAnswer; 23 | mapping(uint256 => uint256) public override getTimestamp; 24 | mapping(uint256 => uint256) private getStartedAt; 25 | 26 | constructor(uint8 _decimals, int256 _initialAnswer) { 27 | decimals = _decimals; 28 | updateAnswer(_initialAnswer); 29 | } 30 | 31 | function updateAnswer(int256 _answer) public { 32 | latestAnswer = _answer; 33 | latestTimestamp = block.timestamp; 34 | latestRound++; 35 | getAnswer[latestRound] = _answer; 36 | getTimestamp[latestRound] = block.timestamp; 37 | getStartedAt[latestRound] = block.timestamp; 38 | } 39 | 40 | function updateRoundData( 41 | uint80 _roundId, 42 | int256 _answer, 43 | uint256 _timestamp, 44 | uint256 _startedAt 45 | ) public { 46 | latestRound = _roundId; 47 | latestAnswer = _answer; 48 | latestTimestamp = _timestamp; 49 | getAnswer[latestRound] = _answer; 50 | getTimestamp[latestRound] = _timestamp; 51 | getStartedAt[latestRound] = _startedAt; 52 | } 53 | 54 | function getRoundData(uint80 _roundId) 55 | external 56 | view 57 | override 58 | returns ( 59 | uint80 roundId, 60 | int256 answer, 61 | uint256 startedAt, 62 | uint256 updatedAt, 63 | uint80 answeredInRound 64 | ) 65 | { 66 | return (_roundId, getAnswer[_roundId], getStartedAt[_roundId], getTimestamp[_roundId], _roundId); 67 | } 68 | 69 | function latestRoundData() 70 | external 71 | view 72 | override 73 | returns ( 74 | uint80 roundId, 75 | int256 answer, 76 | uint256 startedAt, 77 | uint256 updatedAt, 78 | uint80 answeredInRound 79 | ) 80 | { 81 | return ( 82 | uint80(latestRound), 83 | getAnswer[latestRound], 84 | getStartedAt[latestRound], 85 | getTimestamp[latestRound], 86 | uint80(latestRound) 87 | ); 88 | } 89 | 90 | function description() external pure override returns (string memory) { 91 | return "v0.8/tests/MockV3Aggregator.sol"; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contracts/splits/SplitWallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.4; 3 | 4 | import {ISplitMain} from "./interfaces/ISplitMain.sol"; 5 | import {ERC20} from "solmate/src/tokens/ERC20.sol"; 6 | import {SafeTransferLib} from "solmate/src/utils/SafeTransferLib.sol"; 7 | 8 | /** 9 | * ERRORS 10 | */ 11 | 12 | /// @notice Unauthorized sender 13 | error Unauthorized(); 14 | 15 | /** 16 | * @title SplitWallet 17 | * @author 0xSplits 18 | * @notice The implementation logic for `SplitProxy`. 19 | * @dev `SplitProxy` handles `receive()` itself to avoid the gas cost with `DELEGATECALL`. 20 | */ 21 | contract SplitWallet { 22 | using SafeTransferLib for address; 23 | using SafeTransferLib for ERC20; 24 | 25 | /** 26 | * EVENTS 27 | */ 28 | 29 | /** @notice emitted after each successful ETH transfer to proxy 30 | * @param split Address of the split that received ETH 31 | * @param amount Amount of ETH received 32 | */ 33 | event ReceiveETH(address indexed split, uint256 amount); 34 | 35 | /** 36 | * STORAGE 37 | */ 38 | 39 | /** 40 | * STORAGE - CONSTANTS & IMMUTABLES 41 | */ 42 | 43 | /// @notice address of SplitMain for split distributions & EOA/SC withdrawals 44 | ISplitMain public immutable splitMain; 45 | 46 | /** 47 | * MODIFIERS 48 | */ 49 | 50 | /// @notice Reverts if the sender isn't SplitMain 51 | modifier onlySplitMain() { 52 | if (msg.sender != address(splitMain)) revert Unauthorized(); 53 | _; 54 | } 55 | 56 | /** 57 | * CONSTRUCTOR 58 | */ 59 | 60 | constructor() { 61 | splitMain = ISplitMain(msg.sender); 62 | } 63 | 64 | /** 65 | * FUNCTIONS - PUBLIC & EXTERNAL 66 | */ 67 | 68 | /** @notice Sends amount `amount` of ETH in proxy to SplitMain 69 | * @dev payable reduces gas cost; no vulnerability to accidentally lock 70 | * ETH introduced since fn call is restricted to SplitMain 71 | * @param amount Amount to send 72 | */ 73 | function sendETHToMain(uint256 amount) external payable onlySplitMain { 74 | address(splitMain).safeTransferETH(amount); 75 | } 76 | 77 | /** @notice Sends amount `amount` of ERC20 `token` in proxy to SplitMain 78 | * @dev payable reduces gas cost; no vulnerability to accidentally lock 79 | * ETH introduced since fn call is restricted to SplitMain 80 | * @param token Token to send 81 | * @param amount Amount to send 82 | */ 83 | function sendERC20ToMain(ERC20 token, uint256 amount) 84 | external 85 | payable 86 | onlySplitMain 87 | { 88 | token.safeTransfer(address(splitMain), amount); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /contracts/splits/interfaces/ISplitMain.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.4; 3 | 4 | import {ERC20} from "solmate/src/tokens/ERC20.sol"; 5 | 6 | /** 7 | * @title ISplitMain 8 | * @author 0xSplits 9 | */ 10 | interface ISplitMain { 11 | /** 12 | * FUNCTIONS 13 | */ 14 | 15 | function walletImplementation() external returns (address); 16 | 17 | function createSplit( 18 | address[] calldata accounts, 19 | uint32[] calldata percentAllocations, 20 | uint32 distributorFee, 21 | address controller 22 | ) external returns (address); 23 | 24 | function predictImmutableSplitAddress( 25 | address[] calldata accounts, 26 | uint32[] calldata percentAllocations, 27 | uint32 distributorFee 28 | ) external view returns (address); 29 | 30 | function updateSplit( 31 | address split, 32 | address[] calldata accounts, 33 | uint32[] calldata percentAllocations, 34 | uint32 distributorFee 35 | ) external; 36 | 37 | function transferControl(address split, address newController) external; 38 | 39 | function cancelControlTransfer(address split) external; 40 | 41 | function acceptControl(address split) external; 42 | 43 | function makeSplitImmutable(address split) external; 44 | 45 | function distributeETH( 46 | address split, 47 | address[] calldata accounts, 48 | uint32[] calldata percentAllocations, 49 | uint32 distributorFee, 50 | address distributorAddress 51 | ) external; 52 | 53 | function updateAndDistributeETH( 54 | address split, 55 | address[] calldata accounts, 56 | uint32[] calldata percentAllocations, 57 | uint32 distributorFee, 58 | address distributorAddress 59 | ) external; 60 | 61 | function distributeERC20( 62 | address split, 63 | ERC20 token, 64 | address[] calldata accounts, 65 | uint32[] calldata percentAllocations, 66 | uint32 distributorFee, 67 | address distributorAddress 68 | ) external; 69 | 70 | function updateAndDistributeERC20( 71 | address split, 72 | ERC20 token, 73 | address[] calldata accounts, 74 | uint32[] calldata percentAllocations, 75 | uint32 distributorFee, 76 | address distributorAddress 77 | ) external; 78 | 79 | function withdraw( 80 | address account, 81 | uint256 withdrawETH, 82 | ERC20[] calldata tokens 83 | ) external; 84 | 85 | /** 86 | * EVENTS 87 | */ 88 | 89 | /** @notice emitted after each successful split creation 90 | * @param split Address of the created split 91 | */ 92 | event CreateSplit(address indexed split); 93 | 94 | /** @notice emitted after each successful split update 95 | * @param split Address of the updated split 96 | */ 97 | event UpdateSplit(address indexed split); 98 | 99 | /** @notice emitted after each initiated split control transfer 100 | * @param split Address of the split control transfer was initiated for 101 | * @param newPotentialController Address of the split's new potential controller 102 | */ 103 | event InitiateControlTransfer( 104 | address indexed split, 105 | address indexed newPotentialController 106 | ); 107 | 108 | /** @notice emitted after each canceled split control transfer 109 | * @param split Address of the split control transfer was canceled for 110 | */ 111 | event CancelControlTransfer(address indexed split); 112 | 113 | /** @notice emitted after each successful split control transfer 114 | * @param split Address of the split control was transferred for 115 | * @param previousController Address of the split's previous controller 116 | * @param newController Address of the split's new controller 117 | */ 118 | event ControlTransfer( 119 | address indexed split, 120 | address indexed previousController, 121 | address indexed newController 122 | ); 123 | 124 | /** @notice emitted after each successful ETH balance split 125 | * @param split Address of the split that distributed its balance 126 | * @param amount Amount of ETH distributed 127 | * @param distributorAddress Address to credit distributor fee to 128 | */ 129 | event DistributeETH( 130 | address indexed split, 131 | uint256 amount, 132 | address indexed distributorAddress 133 | ); 134 | 135 | /** @notice emitted after each successful ERC20 balance split 136 | * @param split Address of the split that distributed its balance 137 | * @param token Address of ERC20 distributed 138 | * @param amount Amount of ERC20 distributed 139 | * @param distributorAddress Address to credit distributor fee to 140 | */ 141 | event DistributeERC20( 142 | address indexed split, 143 | ERC20 indexed token, 144 | uint256 amount, 145 | address indexed distributorAddress 146 | ); 147 | 148 | /** @notice emitted after each successful withdrawal 149 | * @param account Address that funds were withdrawn to 150 | * @param ethAmount Amount of ETH withdrawn 151 | * @param tokens Addresses of ERC20s withdrawn 152 | * @param tokenAmounts Amounts of corresponding ERC20s withdrawn 153 | */ 154 | event Withdrawal( 155 | address indexed account, 156 | uint256 ethAmount, 157 | ERC20[] tokens, 158 | uint256[] tokenAmounts 159 | ); 160 | } 161 | -------------------------------------------------------------------------------- /contracts/splits/interfaces/ReverseRecords.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.4; 3 | 4 | interface ReverseRecords { 5 | function getNames(address[] calldata addresses) 6 | external 7 | view 8 | returns (string[] memory r); 9 | } 10 | -------------------------------------------------------------------------------- /contracts/splits/libraries/Clones.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8.4; 3 | 4 | /// @notice create opcode failed 5 | error CreateError(); 6 | /// @notice create2 opcode failed 7 | error Create2Error(); 8 | 9 | library Clones { 10 | /** 11 | * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation` 12 | * except when someone calls `receive()` and then it emits an event matching 13 | * `SplitWallet.ReceiveETH(indexed address, amount)` 14 | * Inspired by OZ & 0age's minimal clone implementations based on eip 1167 found at 15 | * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.3.0/contracts/proxy/Clones.sol 16 | * and https://medium.com/coinmonks/the-more-minimal-proxy-5756ae08ee48 17 | * 18 | * This function uses the create2 opcode and a `salt` to deterministically deploy 19 | * the clone. Using the same `implementation` and `salt` multiple time will revert, since 20 | * the clones cannot be deployed twice at the same address. 21 | * 22 | * init: 0x3d605d80600a3d3981f3 23 | * 3d returndatasize 0 24 | * 605d push1 0x5d 0x5d 0 25 | * 80 dup1 0x5d 0x5d 0 26 | * 600a push1 0x0a 0x0a 0x5d 0x5d 0 27 | * 3d returndatasize 0 0x0a 0x5d 0x5d 0 28 | * 39 codecopy 0x5d 0 destOffset offset length memory[destOffset:destOffset+length] = address(this).code[offset:offset+length] copy executing contracts bytecode 29 | * 81 dup2 0 0x5d 0 30 | * f3 return 0 offset length return memory[offset:offset+length] returns from this contract call 31 | * 32 | * contract: 0x36603057343d52307f830d2d700a97af574b186c80d40429385d24241565b08a7c559ba283a964d9b160203da23d3df35b3d3d3d3d363d3d37363d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e605b57fd5bf3 33 | * 0x000 36 calldatasize cds 34 | * 0x001 6030 push1 0x30 0x30 cds 35 | * ,=< 0x003 57 jumpi 36 | * | 0x004 34 callvalue cv 37 | * | 0x005 3d returndatasize 0 cv 38 | * | 0x006 52 mstore 39 | * | 0x007 30 address addr 40 | * | 0x008 7f830d.. push32 0x830d.. id addr 41 | * | 0x029 6020 push1 0x20 0x20 id addr 42 | * | 0x02b 3d returndatasize 0 0x20 id addr 43 | * | 0x02c a2 log2 44 | * | 0x02d 3d returndatasize 0 45 | * | 0x02e 3d returndatasize 0 0 46 | * | 0x02f f3 return 47 | * `-> 0x030 5b jumpdest 48 | * 0x031 3d returndatasize 0 49 | * 0x032 3d returndatasize 0 0 50 | * 0x033 3d returndatasize 0 0 0 51 | * 0x034 3d returndatasize 0 0 0 0 52 | * 0x035 36 calldatasize cds 0 0 0 0 53 | * 0x036 3d returndatasize 0 cds 0 0 0 0 54 | * 0x037 3d returndatasize 0 0 cds 0 0 0 0 55 | * 0x038 37 calldatacopy 0 0 0 0 56 | * 0x039 36 calldatasize cds 0 0 0 0 57 | * 0x03a 3d returndatasize 0 cds 0 0 0 0 58 | * 0x03b 73bebe.. push20 0xbebe.. 0xbebe 0 cds 0 0 0 0 59 | * 0x050 5a gas gas 0xbebe 0 cds 0 0 0 0 60 | * 0x051 f4 delegatecall suc 0 0 61 | * 0x052 3d returndatasize rds suc 0 0 62 | * 0x053 3d returndatasize rds rds suc 0 0 63 | * 0x054 93 swap4 0 rds suc 0 rds 64 | * 0x055 80 dup1 0 0 rds suc 0 rds 65 | * 0x056 3e returndatacopy suc 0 rds 66 | * 0x057 605b push1 0x5b 0x5b suc 0 rds 67 | * ,=< 0x059 57 jumpi 0 rds 68 | * | 0x05a fd revert 69 | * `-> 0x05b 5b jumpdest 0 rds 70 | * 0x05c f3 return 71 | * 72 | */ 73 | function clone(address implementation) internal returns (address instance) { 74 | assembly { 75 | let ptr := mload(0x40) 76 | mstore( 77 | ptr, 78 | 0x3d605d80600a3d3981f336603057343d52307f00000000000000000000000000 79 | ) 80 | mstore( 81 | add(ptr, 0x13), 82 | 0x830d2d700a97af574b186c80d40429385d24241565b08a7c559ba283a964d9b1 83 | ) 84 | mstore( 85 | add(ptr, 0x33), 86 | 0x60203da23d3df35b3d3d3d3d363d3d37363d7300000000000000000000000000 87 | ) 88 | mstore(add(ptr, 0x46), shl(0x60, implementation)) 89 | mstore( 90 | add(ptr, 0x5a), 91 | 0x5af43d3d93803e605b57fd5bf300000000000000000000000000000000000000 92 | ) 93 | instance := create(0, ptr, 0x67) 94 | } 95 | if (instance == address(0)) revert CreateError(); 96 | } 97 | 98 | function cloneDeterministic(address implementation, bytes32 salt) 99 | internal 100 | returns (address instance) 101 | { 102 | assembly { 103 | let ptr := mload(0x40) 104 | mstore( 105 | ptr, 106 | 0x3d605d80600a3d3981f336603057343d52307f00000000000000000000000000 107 | ) 108 | mstore( 109 | add(ptr, 0x13), 110 | 0x830d2d700a97af574b186c80d40429385d24241565b08a7c559ba283a964d9b1 111 | ) 112 | mstore( 113 | add(ptr, 0x33), 114 | 0x60203da23d3df35b3d3d3d3d363d3d37363d7300000000000000000000000000 115 | ) 116 | mstore(add(ptr, 0x46), shl(0x60, implementation)) 117 | mstore( 118 | add(ptr, 0x5a), 119 | 0x5af43d3d93803e605b57fd5bf300000000000000000000000000000000000000 120 | ) 121 | instance := create2(0, ptr, 0x67, salt) 122 | } 123 | if (instance == address(0)) revert Create2Error(); 124 | } 125 | 126 | /** 127 | * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}. 128 | */ 129 | function predictDeterministicAddress( 130 | address implementation, 131 | bytes32 salt, 132 | address deployer 133 | ) internal pure returns (address predicted) { 134 | assembly { 135 | let ptr := mload(0x40) 136 | mstore( 137 | ptr, 138 | 0x3d605d80600a3d3981f336603057343d52307f00000000000000000000000000 139 | ) 140 | mstore( 141 | add(ptr, 0x13), 142 | 0x830d2d700a97af574b186c80d40429385d24241565b08a7c559ba283a964d9b1 143 | ) 144 | mstore( 145 | add(ptr, 0x33), 146 | 0x60203da23d3df35b3d3d3d3d363d3d37363d7300000000000000000000000000 147 | ) 148 | mstore(add(ptr, 0x46), shl(0x60, implementation)) 149 | mstore( 150 | add(ptr, 0x5a), 151 | 0x5af43d3d93803e605b57fd5bf3ff000000000000000000000000000000000000 152 | ) 153 | mstore(add(ptr, 0x68), shl(0x60, deployer)) 154 | mstore(add(ptr, 0x7c), salt) 155 | mstore(add(ptr, 0x9c), keccak256(ptr, 0x67)) 156 | predicted := keccak256(add(ptr, 0x67), 0x55) 157 | } 158 | } 159 | 160 | /** 161 | * @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}. 162 | */ 163 | function predictDeterministicAddress(address implementation, bytes32 salt) 164 | internal 165 | view 166 | returns (address predicted) 167 | { 168 | return predictDeterministicAddress(implementation, salt, address(this)); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /contracts/storage/CrescendoConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | struct CrescendoConfig { 5 | string name; 6 | string symbol; 7 | uint256 initialPrice; 8 | uint256 step1; 9 | uint256 step2; 10 | uint256 hitch; 11 | uint256 takeRateBPS; 12 | uint256 unlockDate; 13 | uint256 saleStart; 14 | uint256 royaltyBPS; 15 | } 16 | -------------------------------------------------------------------------------- /contracts/storage/DCNT721AStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import './TokenGateConfig.sol'; 5 | 6 | contract DCNT721AStorage { 7 | /// @notice token gating 8 | TokenGateConfig public tokenGateConfig; 9 | } -------------------------------------------------------------------------------- /contracts/storage/EditionConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | struct EditionConfig { 5 | string name; 6 | string symbol; 7 | bool hasAdjustableCap; 8 | bool isSoulbound; 9 | uint32 maxTokens; 10 | uint32 maxTokenPurchase; 11 | uint32 presaleStart; 12 | uint32 presaleEnd; 13 | uint32 saleStart; 14 | uint32 saleEnd; 15 | uint16 royaltyBPS; 16 | uint96 tokenPrice; 17 | address feeManager; 18 | address payoutAddress; 19 | bytes32 presaleMerkleRoot; 20 | } 21 | -------------------------------------------------------------------------------- /contracts/storage/MetadataConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | struct MetadataConfig { 5 | string contractURI; 6 | string metadataURI; 7 | bytes metadataRendererInit; 8 | address parentIP; 9 | } 10 | -------------------------------------------------------------------------------- /contracts/storage/TokenGateConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | enum SaleType { 5 | ALL, 6 | PRESALE, 7 | PRIMARY 8 | } 9 | 10 | struct TokenGateConfig { 11 | address tokenAddress; 12 | uint88 minBalance; 13 | SaleType saleType; 14 | } 15 | -------------------------------------------------------------------------------- /contracts/utils/Credits.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | import {IOnChainMetadata} from "../interfaces/IOnChainMetadata.sol"; 18 | import {MetadataRenderAdminCheck} from "./MetadataRenderAdminCheck.sol"; 19 | 20 | contract Credits is MetadataRenderAdminCheck, IOnChainMetadata { 21 | /// @notice Array of credits 22 | mapping(address => Credit[]) internal credits; 23 | 24 | /// @notice Admin function to update description 25 | /// @param target target description 26 | /// @param _credits credits for the track 27 | function updateCredits(address target, Credit[] calldata _credits) 28 | public 29 | requireSenderAdmin(target) 30 | { 31 | delete credits[target]; 32 | 33 | for (uint256 i = 0; i < _credits.length; i++) { 34 | credits[target].push( 35 | Credit(_credits[i].name, _credits[i].collaboratorType) 36 | ); 37 | } 38 | 39 | emit CreditsUpdated({ 40 | target: target, 41 | sender: msg.sender, 42 | credits: _credits 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contracts/utils/MetadataRenderAdminCheck.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | contract MetadataRenderAdminCheck { 7 | error Access_OnlyAdmin(); 8 | 9 | /// @notice Modifier to require the sender to be an admin 10 | /// @param target address that the user wants to modify 11 | modifier requireSenderAdmin(address target) { 12 | if (target != msg.sender && Ownable(target).owner() != msg.sender) { 13 | revert Access_OnlyAdmin(); 14 | } 15 | 16 | _; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /contracts/utils/MusicMetadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | /* 5 | ______ _______ _______ _______ _ _________ 6 | ( __ \ ( ____ \( ____ \( ____ \( ( /|\__ __/ 7 | | ( \ )| ( \/| ( \/| ( \/| \ ( | ) ( 8 | | | ) || (__ | | | (__ | \ | | | | 9 | | | | || __) | | | __) | (\ \) | | | 10 | | | ) || ( | | | ( | | \ | | | 11 | | (__/ )| (____/\| (____/\| (____/\| ) \ | | | 12 | (______/ (_______/(_______/(_______/|/ )_) )_( 13 | 14 | */ 15 | 16 | /// ============ Imports ============ 17 | import {IOnChainMetadata} from "../interfaces/IOnChainMetadata.sol"; 18 | import {MetadataRenderAdminCheck} from "./MetadataRenderAdminCheck.sol"; 19 | 20 | contract MusicMetadata is MetadataRenderAdminCheck, IOnChainMetadata { 21 | mapping(address => SongMetadata) public songMetadatas; 22 | mapping(address => ProjectMetadata) public projectMetadatas; 23 | mapping(address => string[]) internal trackTags; 24 | 25 | /// @notice Update media URIs 26 | /// @param target target for contract to update metadata for 27 | /// @param imageURI new image uri address 28 | /// @param animationURI new animation uri address 29 | function updateMediaURIs( 30 | address target, 31 | string memory imageURI, 32 | string memory animationURI 33 | ) external requireSenderAdmin(target) { 34 | songMetadatas[target].song.artwork.artworkUri = imageURI; 35 | songMetadatas[target].song.audio.losslessAudio = animationURI; 36 | emit MediaURIsUpdated({ 37 | target: target, 38 | sender: msg.sender, 39 | imageURI: imageURI, 40 | animationURI: animationURI 41 | }); 42 | } 43 | 44 | /// @notice Admin function to update description 45 | /// @param target target description 46 | /// @param newDescription new description 47 | function updateDescription(address target, string memory newDescription) 48 | external 49 | requireSenderAdmin(target) 50 | { 51 | songMetadatas[target].songPublishingData.description = newDescription; 52 | 53 | emit DescriptionUpdated({ 54 | target: target, 55 | sender: msg.sender, 56 | newDescription: newDescription 57 | }); 58 | } 59 | 60 | /// @notice Admin function to update description 61 | /// @param target target description 62 | /// @param tags The tags of the track 63 | function updateTags(address target, string[] memory tags) 64 | public 65 | requireSenderAdmin(target) 66 | { 67 | trackTags[target] = tags; 68 | 69 | emit TagsUpdated({target: target, sender: msg.sender, tags: tags}); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /contracts/utils/OperatorFilterer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {IOperatorFilterRegistry} from "operator-filter-registry/src/IOperatorFilterRegistry.sol"; 5 | 6 | /** 7 | * @title OperatorFilterer 8 | * @notice Abstract contract whose constructor automatically registers and optionally subscribes to or copies another 9 | * registrant's entries in the OperatorFilterRegistry. 10 | * @dev This smart contract is meant to be inherited by token contracts so they can use the following: 11 | * - `onlyAllowedOperator` modifier for `transferFrom` and `safeTransferFrom` methods. 12 | * - `onlyAllowedOperatorApproval` modifier for `approve` and `setApprovalForAll` methods. 13 | */ 14 | abstract contract OperatorFilterer { 15 | error OperatorNotAllowed(address operator); 16 | 17 | IOperatorFilterRegistry public constant operatorFilterRegistry = 18 | IOperatorFilterRegistry(0x000000000000AAeB6D7670E522A718067333cd4E); 19 | 20 | modifier onlyAllowedOperator(address from) virtual { 21 | // Allow spending tokens from addresses with balance 22 | // Note that this still allows listings and marketplaces with escrow to transfer tokens if transferred 23 | // from an EOA. 24 | if (from != msg.sender) { 25 | _checkFilterOperator(msg.sender); 26 | } 27 | _; 28 | } 29 | 30 | modifier onlyAllowedOperatorApproval(address operator) virtual { 31 | _checkFilterOperator(operator); 32 | _; 33 | } 34 | 35 | function _checkFilterOperator(address operator) internal view virtual { 36 | // Check registry code length to facilitate testing in environments without a deployed registry. 37 | if (address(operatorFilterRegistry).code.length > 0) { 38 | if (!operatorFilterRegistry.isOperatorAllowed(address(this), operator)) { 39 | revert OperatorNotAllowed(operator); 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /contracts/utils/Pausable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Contract module which allows children to implement an emergency stop 8 | * mechanism that can be triggered by an authorized account. 9 | * 10 | * This module is used through inheritance. It will make available the 11 | * modifiers `whenNotPaused` and `whenPaused`, which can be applied to 12 | * the functions of your contract. Note that they will not be pausable by 13 | * simply including this module, only once the modifiers are put in place. 14 | */ 15 | abstract contract Pausable { 16 | /** 17 | * @dev Indicates whether the contract is currently paused or not. 18 | */ 19 | bool private _paused; 20 | 21 | /** 22 | * @dev Error thrown when the contract is paused and an operation is attempted. 23 | */ 24 | error Paused(); 25 | 26 | /** 27 | * @dev Error thrown when the contract is not paused and an operation is attempted. 28 | */ 29 | error NotPaused(); 30 | 31 | /** 32 | * @dev Initializes the contract in unpaused state. 33 | */ 34 | constructor() { 35 | _paused = false; 36 | } 37 | 38 | /** 39 | * @dev Modifier to make a function callable only when the contract is not paused. 40 | * 41 | * Requirements: 42 | * 43 | * - The contract must not be paused. 44 | */ 45 | modifier whenNotPaused() { 46 | _requireNotPaused(); 47 | _; 48 | } 49 | 50 | /** 51 | * @dev Modifier to make a function callable only when the contract is paused. 52 | * 53 | * Requirements: 54 | * 55 | * - The contract must be paused. 56 | */ 57 | modifier whenPaused() { 58 | _requirePaused(); 59 | _; 60 | } 61 | 62 | /** 63 | * @dev Returns true if the contract is paused, and false otherwise. 64 | */ 65 | function paused() public view virtual returns (bool) { 66 | return _paused; 67 | } 68 | 69 | /** 70 | * @dev Throws if the contract is paused. 71 | */ 72 | function _requireNotPaused() internal view virtual { 73 | if ( paused() ) revert Paused(); 74 | } 75 | 76 | /** 77 | * @dev Throws if the contract is not paused. 78 | */ 79 | function _requirePaused() internal view virtual { 80 | if ( ! paused() ) revert NotPaused(); 81 | } 82 | 83 | /** 84 | * @dev Triggers stopped state. 85 | * 86 | * Requirements: 87 | * 88 | * - The contract must not be paused. 89 | */ 90 | function _pause() internal virtual whenNotPaused { 91 | _paused = true; 92 | } 93 | 94 | /** 95 | * @dev Returns to normal state. 96 | * 97 | * Requirements: 98 | * 99 | * - The contract must be paused. 100 | */ 101 | function _unpause() internal virtual whenPaused { 102 | _paused = false; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /contracts/utils/Splits.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later 2 | pragma solidity ^0.8; 3 | 4 | import {ERC20} from "solmate/src/tokens/ERC20.sol"; 5 | import "../splits/interfaces/ISplitMain.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | 8 | abstract contract Splits is Ownable { 9 | 10 | address public splitMain; 11 | address public splitWallet; 12 | 13 | function createSplit( 14 | address _splitMain, 15 | address[] calldata accounts, 16 | uint32[] calldata percentAllocations, 17 | uint32 distributorFee 18 | ) public virtual onlyOwner { 19 | require(splitWallet == address(0), "Split already created"); 20 | splitMain = _splitMain; 21 | splitWallet = ISplitMain(splitMain).createSplit( 22 | accounts, 23 | percentAllocations, 24 | distributorFee, 25 | msg.sender 26 | ); 27 | } 28 | 29 | function distributeETH( 30 | address[] calldata accounts, 31 | uint32[] calldata percentAllocations, 32 | uint32 distributorFee, 33 | address distributorAddress 34 | ) public virtual requireSplit { 35 | _transferETHToSplit(); 36 | ISplitMain(splitMain).distributeETH( 37 | splitWallet, 38 | accounts, 39 | percentAllocations, 40 | distributorFee, 41 | distributorAddress 42 | ); 43 | } 44 | 45 | function distributeERC20( 46 | ERC20 token, 47 | address[] calldata accounts, 48 | uint32[] calldata percentAllocations, 49 | uint32 distributorFee, 50 | address distributorAddress 51 | ) public virtual requireSplit { 52 | _transferERC20ToSplit(token); 53 | ISplitMain(splitMain).distributeERC20( 54 | splitWallet, 55 | token, 56 | accounts, 57 | percentAllocations, 58 | distributorFee, 59 | distributorAddress 60 | ); 61 | } 62 | 63 | function distributeAndWithdraw( 64 | address account, 65 | uint256 withdrawETH, 66 | ERC20[] memory tokens, 67 | address[] calldata accounts, 68 | uint32[] calldata percentAllocations, 69 | uint32 distributorFee, 70 | address distributorAddress 71 | ) public virtual requireSplit { 72 | if (withdrawETH != 0) { 73 | distributeETH( 74 | accounts, 75 | percentAllocations, 76 | distributorFee, 77 | distributorAddress 78 | ); 79 | } 80 | 81 | for (uint256 i = 0; i < tokens.length; ++i) { 82 | distributeERC20( 83 | tokens[i], 84 | accounts, 85 | percentAllocations, 86 | distributorFee, 87 | distributorAddress 88 | ); 89 | } 90 | 91 | _withdraw(account, withdrawETH, tokens); 92 | } 93 | 94 | function transferToSplit(uint256 transferETH, ERC20[] memory tokens) 95 | public 96 | virtual 97 | requireSplit 98 | { 99 | if (transferETH != 0) { 100 | _transferETHToSplit(); 101 | } 102 | 103 | for (uint256 i = 0; i < tokens.length; ++i) { 104 | _transferERC20ToSplit(tokens[i]); 105 | } 106 | } 107 | 108 | function _transferETHToSplit() internal virtual { 109 | (bool success, ) = splitWallet.call{value: address(this).balance}(""); 110 | require(success, "Could not transfer ETH to split"); 111 | } 112 | 113 | function _transferERC20ToSplit(ERC20 token) internal virtual { 114 | uint256 balance = token.balanceOf(address(this)); 115 | token.transfer(splitWallet, balance); 116 | } 117 | 118 | function _withdraw( 119 | address account, 120 | uint256 withdrawETH, 121 | ERC20[] memory tokens 122 | ) internal virtual { 123 | ISplitMain(splitMain).withdraw( 124 | account, 125 | withdrawETH, 126 | tokens 127 | ); 128 | } 129 | 130 | modifier requireSplit() { 131 | require(splitWallet != address(0), "Split not created yet"); 132 | _; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /contracts/utils/Version.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | 4 | contract Version { 5 | uint32 private immutable _version; 6 | 7 | /// @notice The version of the contract 8 | /// @return The version ID of this contract implementation 9 | function contractVersion() external view returns (uint32) { 10 | return _version; 11 | } 12 | 13 | constructor(uint32 version) { 14 | _version = version; 15 | } 16 | } -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | require('hardhat-contract-sizer'); 4 | require("dotenv").config(); 5 | import './tasks' 6 | 7 | const config: HardhatUserConfig = { 8 | solidity: { 9 | version: "0.8.17", 10 | settings: { 11 | optimizer: { 12 | enabled: true, 13 | runs: 200 14 | } 15 | } 16 | }, 17 | networks: { 18 | mainnet: { 19 | url: process.env.MAINNET_URL, 20 | accounts: [process.env.PRIVATE_KEY as string], 21 | }, 22 | goerli: { 23 | url: process.env.GOERLI_URL, 24 | accounts: [process.env.PRIVATE_KEY as string] 25 | }, 26 | sepolia: { 27 | url: process.env.SEPOLIA_URL, 28 | accounts: [process.env.PRIVATE_KEY as string] 29 | }, 30 | polygon: { 31 | url: process.env.POLYGON_MAINNET_URL, 32 | accounts: [process.env.PRIVATE_KEY as string] 33 | }, 34 | polygon_testnet: { 35 | url: process.env.POLYGON_TESTNET_URL, 36 | accounts: [process.env.PRIVATE_KEY as string] 37 | }, 38 | optimism: { 39 | url: process.env.OPTIMISM_MAINNET_URL, 40 | accounts: [process.env.PRIVATE_KEY as string] 41 | }, 42 | optimism_testnet: { 43 | url: process.env.OPTIMISM_TESTNET_URL, 44 | accounts: [process.env.PRIVATE_KEY as string] 45 | }, 46 | arbitrum: { 47 | url: process.env.ARBITRUM_MAINNET_URL, 48 | accounts: [process.env.PRIVATE_KEY as string] 49 | }, 50 | arbitrum_testnet: { 51 | url: process.env.ARBITRUM_TESTNET_URL, 52 | accounts: [process.env.PRIVATE_KEY as string] 53 | }, 54 | base: { 55 | url: process.env.BASE_MAINNET_URL, 56 | accounts: [process.env.PRIVATE_KEY as string], 57 | gasPrice: 1000000000, 58 | }, 59 | base_testnet: { 60 | url: process.env.BASE_TESTNET_URL, 61 | accounts: [process.env.PRIVATE_KEY as string], 62 | gasPrice: 1000000000, 63 | }, 64 | zora: { 65 | url: process.env.ZORA_MAINNET_URL, 66 | accounts: [process.env.PRIVATE_KEY as string], 67 | }, 68 | zora_testnet: { 69 | url: process.env.ZORA_TESTNET_URL, 70 | accounts: [process.env.PRIVATE_KEY as string], 71 | }, 72 | }, 73 | etherscan: { 74 | apiKey: { 75 | mainnet: process.env.ETHERSCAN_KEY as string, 76 | goerli: process.env.ETHERSCAN_KEY as string, 77 | sepolia: process.env.ETHERSCAN_KEY as string, 78 | polygon: process.env.POLYGONSCAN_KEY as string, 79 | polygonMumbai: process.env.POLYGONSCAN_KEY as string, 80 | optimisticEthereum: process.env.OPTIMISMSCAN_KEY as string, 81 | optimism_testnet: process.env.OPTIMISMSCAN_KEY as string, 82 | arbitrumOne: process.env.ARBISCAN_KEY as string, 83 | arbitrum_testnet: process.env.ARBISCAN_KEY as string, 84 | base: process.env.BASESCAN_KEY as string, 85 | base_testnet: process.env.BASESCAN_KEY as string, 86 | zora: process.env.BLOCKSCOUT_KEY as string, 87 | zora_testnet: process.env.BLOCKSCOUT_KEY as string, 88 | }, 89 | customChains: [ 90 | { 91 | network: "optimism_testnet", 92 | chainId: 420, 93 | urls: { 94 | apiURL: "https://api-goerli-optimism.etherscan.io/api", 95 | browserURL: "https://goerli-optimism.etherscan.io/" 96 | } 97 | }, 98 | { 99 | network: "arbitrum_testnet", 100 | chainId: 421613, 101 | urls: { 102 | apiURL: "https://api-goerli.arbiscan.io/api", 103 | browserURL: "https://goerli.arbiscan.io/" 104 | } 105 | }, 106 | { 107 | network: "base", 108 | chainId: 8453, 109 | urls: { 110 | apiURL: "https://api.basescan.org/api", 111 | browserURL: "https://basescan.org" 112 | } 113 | }, 114 | { 115 | network: "base_testnet", 116 | chainId: 84531, 117 | urls: { 118 | apiURL: "https://api-goerli.basescan.org/api", 119 | browserURL: "https://goerli.basescan.org" 120 | } 121 | }, 122 | { 123 | network: "zora", 124 | chainId: 7777777, 125 | urls: { 126 | apiURL: "https://explorer.zora.energy/api", 127 | browserURL: "https://explorer.zora.energy" 128 | } 129 | }, 130 | { 131 | network: "zora_testnet", 132 | chainId: 999, 133 | urls: { 134 | apiURL: "https://testnet.explorer.zora.energy/api", 135 | browserURL: "https://testnet.explorer.zora.energy" 136 | } 137 | } 138 | ], 139 | }, 140 | gasReporter: { 141 | currency: 'USD', 142 | coinmarketcap: process.env.COINMARKETCAP_KEY, 143 | enabled: false, 144 | gasPrice: 15 145 | }, 146 | contractSizer: { 147 | alphaSort: true, 148 | disambiguatePaths: false, 149 | runOnCompile: false, 150 | strict: true, 151 | only: [':DCNT'], 152 | } 153 | }; 154 | 155 | export default config; 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier": { 3 | "tabWidth": 2 4 | }, 5 | "dependencies": { 6 | "@chainlink/contracts": "^0.6.1", 7 | "@openzeppelin/contracts": "^4.7.2", 8 | "dotenv": "^16.0.1", 9 | "erc721a": "^4.2.2", 10 | "hardhat": "^2.12.3", 11 | "keccak256": "^1.0.6", 12 | "merkletreejs": "^0.3.0", 13 | "operator-filter-registry": "^1.3.1", 14 | "solmate": "^6.5.0" 15 | }, 16 | "devDependencies": { 17 | "@nomicfoundation/hardhat-toolbox": "^1.0.2", 18 | "hardhat-contract-sizer": "^2.8.0", 19 | "prettier": "^2.7.1", 20 | "prettier-plugin-solidity": "^1.0.0-beta.24" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scripts/deployDCNT4907A.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { deployDCNT4907A, theFuture } from "../core"; 3 | 4 | // set up DCNTSDK 5 | const DCNTSDK_ENDPOINT = ''; 6 | 7 | // set up DCNT4907A 8 | const name = 'Token Name'; 9 | const symbol = 'TOKEN'; 10 | const hasAdjustableCap = false; 11 | const isSoulbound = false; 12 | const maxTokens = 25; 13 | const tokenPrice = ethers.utils.parseEther('0.01'); 14 | const maxTokenPurchase = 2; 15 | const presaleMerkleRoot = ethers.constants.HashZero; 16 | const presaleStart = theFuture.time(); 17 | const presaleEnd = theFuture.time(); 18 | const saleStart = theFuture.time(); 19 | const saleEnd = theFuture.time() + theFuture.oneMonth; 20 | const royaltyBPS = 10_00; 21 | const payoutAddress = ethers.constants.AddressZero; 22 | const contractURI = 'http://localhost/contract/'; 23 | const metadataURI = "http://localhost/metadata/"; 24 | const metadataRendererInit = { 25 | description: "This is the description for TOKEN.", 26 | imageURI: "http://localhost/image.jpg", 27 | animationURI: "http://localhost/song.mp3", 28 | }; 29 | const tokenGateConfig = { 30 | tokenAddress: ethers.constants.AddressZero, 31 | minBalance: 0, 32 | saleType: 0, 33 | } 34 | 35 | async function main() { 36 | const DCNTSDK = await ethers.getContractAt("DCNTSDK", DCNTSDK_ENDPOINT); 37 | const DCNT4907A = await deployDCNT4907A( 38 | DCNTSDK, 39 | name, 40 | symbol, 41 | hasAdjustableCap, 42 | isSoulbound, 43 | maxTokens, 44 | tokenPrice, 45 | maxTokenPurchase, 46 | presaleMerkleRoot, 47 | presaleStart, 48 | presaleEnd, 49 | saleStart, 50 | saleEnd, 51 | royaltyBPS, 52 | payoutAddress, 53 | contractURI, 54 | metadataURI, 55 | metadataRendererInit, 56 | tokenGateConfig 57 | ); 58 | console.log("DCNT4907A deployed to: ", DCNT4907A.address); 59 | } 60 | 61 | // We recommend this pattern to be able to use async/await everywhere 62 | // and properly handle errors. 63 | main().catch((error) => { 64 | console.error(error); 65 | process.exitCode = 1; 66 | }); 67 | -------------------------------------------------------------------------------- /scripts/deployDCNT721A.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { deployDCNT721A, theFuture } from "../core"; 3 | 4 | // set up DCNTSDK 5 | const DCNTSDK_ENDPOINT = ''; 6 | 7 | // set up DCNT721A 8 | const name = 'Token Name'; 9 | const symbol = 'TOKEN'; 10 | const hasAdjustableCap = false; 11 | const isSoulbound = false; 12 | const maxTokens = 25; 13 | const tokenPrice = ethers.utils.parseEther('0.01'); 14 | const maxTokenPurchase = 2; 15 | const presaleMerkleRoot = ethers.constants.HashZero; 16 | const presaleStart = theFuture.time(); 17 | const presaleEnd = theFuture.time(); 18 | const saleStart = theFuture.time(); 19 | const saleEnd = theFuture.time() + theFuture.oneMonth; 20 | const royaltyBPS = 10_00; 21 | const payoutAddress = ethers.constants.AddressZero; 22 | const contractURI = 'http://localhost/contract/'; 23 | const metadataURI = "http://localhost/metadata/"; 24 | const metadataRendererInit = { 25 | description: "This is the description for TOKEN.", 26 | imageURI: "http://localhost/image.jpg", 27 | animationURI: "http://localhost/song.mp3", 28 | }; 29 | const tokenGateConfig = { 30 | tokenAddress: ethers.constants.AddressZero, 31 | minBalance: 0, 32 | saleType: 0, 33 | } 34 | 35 | async function main() { 36 | const DCNTSDK = await ethers.getContractAt("DCNTSDK", DCNTSDK_ENDPOINT); 37 | const DCNT721A = await deployDCNT721A( 38 | DCNTSDK, 39 | name, 40 | symbol, 41 | hasAdjustableCap, 42 | isSoulbound, 43 | maxTokens, 44 | tokenPrice, 45 | maxTokenPurchase, 46 | presaleMerkleRoot, 47 | presaleStart, 48 | presaleEnd, 49 | saleStart, 50 | saleEnd, 51 | royaltyBPS, 52 | payoutAddress, 53 | contractURI, 54 | metadataURI, 55 | metadataRendererInit, 56 | tokenGateConfig 57 | ); 58 | console.log("DCNT721A deployed to: ", DCNT721A.address); 59 | } 60 | 61 | // We recommend this pattern to be able to use async/await everywhere 62 | // and properly handle errors. 63 | main().catch((error) => { 64 | console.error(error); 65 | process.exitCode = 1; 66 | }); 67 | -------------------------------------------------------------------------------- /scripts/deployDCNTCrescendo.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { deployDCNTCrescendo, theFuture } from "../core"; 3 | 4 | // set up DCNTSDK 5 | const DCNTSDK_ENDPOINT = ''; 6 | 7 | // set up DCNTCrescendo 8 | const name = 'Token Name'; 9 | const symbol = 'TOKEN'; 10 | const initialPrice = ethers.utils.parseEther('0.05'); 11 | const step1 = ethers.utils.parseEther("0.005"); 12 | const step2 = ethers.utils.parseEther("0.05"); 13 | const hitch = 20; 14 | const takeRateBPS = 15_00; 15 | const unlockDate = theFuture.time(); 16 | const saleStart = theFuture.time(); 17 | const royaltyBPS = 10_00; 18 | const contractURI = 'http://localhost/contract/'; 19 | const metadataURI = "http://localhost/{id}.json"; 20 | const metadataRendererInit = { 21 | description: "This is the description for TOKEN.", 22 | imageURI: "http://localhost/image.jpg", 23 | animationURI: "http://localhost/song.mp3", 24 | }; 25 | 26 | async function main() { 27 | const DCNTSDK = await ethers.getContractAt("DCNTSDK", DCNTSDK_ENDPOINT); 28 | const DCNTCrescendo = await deployDCNTCrescendo( 29 | DCNTSDK, 30 | name, 31 | symbol, 32 | initialPrice, 33 | step1, 34 | step2, 35 | hitch, 36 | takeRateBPS, 37 | unlockDate, 38 | saleStart, 39 | royaltyBPS, 40 | contractURI, 41 | metadataURI, 42 | metadataRendererInit 43 | ); 44 | console.log("DCNTCrescendo deployed to: ", DCNTCrescendo.address); 45 | } 46 | 47 | // We recommend this pattern to be able to use async/await everywhere 48 | // and properly handle errors. 49 | main().catch((error) => { 50 | console.error(error); 51 | process.exitCode = 1; 52 | }); 53 | -------------------------------------------------------------------------------- /scripts/deployDCNTMetadataRenderer.ts: -------------------------------------------------------------------------------- 1 | import { deployContract, deployDCNTMetadataRenderer } from "../core"; 2 | 3 | async function main() { 4 | const sharedNFTLogic = await deployContract('SharedNFTLogic'); 5 | console.log("SharedNFTLogic deployed to: ", sharedNFTLogic.address); 6 | 7 | const metadataRenderer = await deployDCNTMetadataRenderer(sharedNFTLogic); 8 | console.log("DCNTMetadataRenderer deployed to: ", metadataRenderer.address); 9 | } 10 | 11 | // We recommend this pattern to be able to use async/await everywhere 12 | // and properly handle errors. 13 | main().catch((error) => { 14 | console.error(error); 15 | process.exitCode = 1; 16 | }); 17 | -------------------------------------------------------------------------------- /scripts/deployDCNTRegistry.ts: -------------------------------------------------------------------------------- 1 | import { deployContract } from "../core"; 2 | 3 | async function main() { 4 | const registry = await deployContract('DCNTRegistry'); 5 | console.log("DCNTRegistry deployed to: ", registry.address); 6 | } 7 | 8 | // We recommend this pattern to be able to use async/await everywhere 9 | // and properly handle errors. 10 | main().catch((error) => { 11 | console.error(error); 12 | process.exitCode = 1; 13 | }); 14 | -------------------------------------------------------------------------------- /scripts/deployDCNTRentalMarket.ts: -------------------------------------------------------------------------------- 1 | import { deployContract } from "../core"; 2 | 3 | async function main() { 4 | const rentalMarket = await deployContract('DCNTRentalMarket'); 5 | console.log("DCNTRentalMarket deployed to: ", rentalMarket.address); 6 | } 7 | 8 | // We recommend this pattern to be able to use async/await everywhere 9 | // and properly handle errors. 10 | main().catch((error) => { 11 | console.error(error); 12 | process.exitCode = 1; 13 | }); 14 | -------------------------------------------------------------------------------- /scripts/deployDCNTSDK.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network } from "hardhat"; 2 | import { deployDCNTSDK, deployContract, deployDCNTVaultNFT } from "../core"; 3 | 4 | async function main() { 5 | const sdk = await deployDCNTSDK( 6 | null, // proxy implementations 7 | null, // metadata renderer 8 | null, // contract registry 9 | ); 10 | 11 | const metadataRenderer = await ethers.getContractAt('DCNTMetadataRenderer', await sdk.metadataRenderer()); 12 | const registry = await ethers.getContractAt('DCNTRegistry', await sdk.contractRegistry()); 13 | const vaultNFT = await deployDCNTVaultNFT(sdk); 14 | const rentalMarket = await deployContract('DCNTRentalMarket'); 15 | 16 | const contracts = { 17 | DCNTSDK: sdk.address, 18 | DCNT721A: await sdk.DCNT721AImplementation(), 19 | DCNT4907A: await sdk.DCNT4907AImplementation(), 20 | DCNTSeries: await sdk.DCNTSeriesImplementation(), 21 | DCNTCrescendo: await sdk.DCNTCrescendoImplementation(), 22 | DCNTVault: await sdk.DCNTVaultImplementation(), 23 | DCNTStaking: await sdk.DCNTStakingImplementation(), 24 | DCNTMetadataRenderer: metadataRenderer.address, 25 | DCNTRegistry: registry.address, 26 | DCNTVaultNFT: vaultNFT.address, 27 | DCNTRentalMarket: rentalMarket.address, 28 | ZKEdition: await sdk.ZKEditionImplementation(), 29 | } 30 | 31 | console.log(JSON.stringify(contracts, null, 2)); 32 | } 33 | 34 | // We recommend this pattern to be able to use async/await everywhere 35 | // and properly handle errors. 36 | main().catch((error) => { 37 | console.error(error); 38 | process.exitCode = 1; 39 | }); 40 | -------------------------------------------------------------------------------- /scripts/deployDCNTSeries.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { deployDCNTSeries, theFuture } from "../core"; 3 | 4 | // set up DCNTSDK 5 | const DCNTSDK_ENDPOINT = ''; 6 | 7 | // set up DCNT721A 8 | const name = 'Token Name'; 9 | const symbol = 'TOKEN'; 10 | const hasAdjustableCaps = false; 11 | const isSoulbound = false; 12 | const maxTokens = 25; 13 | const tokenPrice = 1; 14 | const maxTokensPerOwner = 5; 15 | const presaleMerkleRoot = ethers.constants.HashZero; 16 | const presaleStart = theFuture.time(); 17 | const presaleEnd = theFuture.time(); 18 | const saleStart = theFuture.time(); 19 | const saleEnd = theFuture.time() + theFuture.oneMonth; 20 | const startTokenId = 0; 21 | const endTokenId = 1; 22 | const royaltyBPS = 10_00; 23 | const payoutAddress = ethers.constants.AddressZero; 24 | const feeManager = ethers.constants.AddressZero; 25 | const currencyOracle = ethers.constants.AddressZero; 26 | const contractURI = 'http://localhost/contract/'; 27 | const metadataURI = "http://localhost/metadata/"; 28 | const tokenGateConfig = { 29 | tokenAddress: ethers.constants.AddressZero, 30 | minBalance: 0, 31 | saleType: 0, 32 | } 33 | 34 | async function main() { 35 | const DCNTSDK = await ethers.getContractAt("DCNTSDK", DCNTSDK_ENDPOINT); 36 | const DCNTSeries = await deployDCNTSeries( 37 | DCNTSDK, 38 | name, 39 | symbol, 40 | hasAdjustableCaps, 41 | isSoulbound, 42 | startTokenId, 43 | endTokenId, 44 | royaltyBPS, 45 | feeManager, 46 | payoutAddress, 47 | currencyOracle, 48 | contractURI, 49 | metadataURI, 50 | { 51 | maxTokens, 52 | tokenPrice, 53 | maxTokensPerOwner, 54 | presaleMerkleRoot, 55 | presaleStart, 56 | presaleEnd, 57 | saleStart, 58 | saleEnd, 59 | tokenGateConfig, 60 | } 61 | ); 62 | console.log("DCNTSeries deployed to: ", DCNTSeries.address); 63 | } 64 | 65 | // We recommend this pattern to be able to use async/await everywhere 66 | // and properly handle errors. 67 | main().catch((error) => { 68 | console.error(error); 69 | process.exitCode = 1; 70 | }); 71 | -------------------------------------------------------------------------------- /scripts/deployDCNTStaking.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { deployDCNTStaking } from "../core"; 3 | 4 | // set up DCNTSDK 5 | const DCNTSDK_ENDPOINT = ''; 6 | 7 | // set up DCNTStaking 8 | const nftAddress = ethers.constants.AddressZero; 9 | const tokenAddress = ethers.constants.AddressZero; 10 | const vaultDuration = 100; 11 | const totalSupply = 25; 12 | 13 | async function main() { 14 | const DCNTSDK = await ethers.getContractAt("DCNTSDK", DCNTSDK_ENDPOINT); 15 | const DCNTStaking = await deployDCNTStaking( 16 | DCNTSDK, 17 | nftAddress, 18 | tokenAddress, 19 | vaultDuration, 20 | totalSupply 21 | ); 22 | console.log("DCNTStaking deployed to: ", DCNTStaking.address); 23 | } 24 | 25 | // We recommend this pattern to be able to use async/await everywhere 26 | // and properly handle errors. 27 | main().catch((error) => { 28 | console.error(error); 29 | process.exitCode = 1; 30 | }); 31 | -------------------------------------------------------------------------------- /scripts/deployDCNTVault.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { deployDCNTVault, theFuture } from "../core"; 3 | 4 | // set up DCNTSDK 5 | const DCNTSDK_ENDPOINT = ''; 6 | 7 | // set up DCNTVault 8 | const vaultDistributionTokenAddress = ethers.constants.AddressZero; 9 | const nftVaultKeyAddress = ethers.constants.AddressZero; 10 | const nftTotalSupply = 25; 11 | const unlockDate = theFuture.time() + theFuture.oneMonth; 12 | 13 | async function main() { 14 | const DCNTSDK = await ethers.getContractAt("DCNTSDK", DCNTSDK_ENDPOINT); 15 | const DCNTVault = await deployDCNTVault( 16 | DCNTSDK, 17 | vaultDistributionTokenAddress, 18 | nftVaultKeyAddress, 19 | nftTotalSupply, 20 | unlockDate 21 | ); 22 | console.log("DCNTVault deployed to: ", DCNTVault.address); 23 | } 24 | 25 | // We recommend this pattern to be able to use async/await everywhere 26 | // and properly handle errors. 27 | main().catch((error) => { 28 | console.error(error); 29 | process.exitCode = 1; 30 | }); 31 | -------------------------------------------------------------------------------- /scripts/deployDCNTVaultNFT.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { deployDCNTVaultNFT } from "../core"; 3 | 4 | // set up DCNTSDK 5 | const DCNTSDK_ENDPOINT = ''; 6 | 7 | async function main() { 8 | const DCNTSDK = await ethers.getContractAt("DCNTSDK", DCNTSDK_ENDPOINT); 9 | const dcntVaultNFT = await deployDCNTVaultNFT(DCNTSDK); 10 | console.log("DCNTVaultNFT deployed to: ", dcntVaultNFT.address); 11 | } 12 | 13 | // We recommend this pattern to be able to use async/await everywhere 14 | // and properly handle errors. 15 | main().catch((error) => { 16 | console.error(error); 17 | process.exitCode = 1; 18 | }); 19 | -------------------------------------------------------------------------------- /scripts/deployFeeManager.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network } from "hardhat"; 2 | import { deployContract } from "../core"; 3 | 4 | export const getFees = () => { 5 | const fees = new Map(); 6 | fees.set('localhost', { fee: '0.00077', commissionBPS: 0 }); 7 | fees.set('mainnet', { fee: '0.00077', commissionBPS: 0 }); 8 | fees.set('goerli', { fee: '0.00077', commissionBPS: 0 }); 9 | fees.set('sepolia', { fee: '0.00077', commissionBPS: 0 }); 10 | fees.set('polygon', { fee: '0.81', commissionBPS: 0 }); 11 | fees.set('polygon_testnet', { fee: '0.81', commissionBPS: 0 }); 12 | fees.set('optimism', { fee: '0.00044', commissionBPS: 0 }); 13 | fees.set('optimism_testnet', { fee: '0.00044', commissionBPS: 0 }); 14 | fees.set('arbitrum', { fee: '0.00044', commissionBPS: 0 }); 15 | fees.set('arbitrum_testnet', { fee: '0.00044', commissionBPS: 0 }); 16 | fees.set('base', { fee: '0.00044', commissionBPS: 0 }); 17 | fees.set('base_testnet', { fee: '0.00044', commissionBPS: 0 }); 18 | fees.set('zora', { fee: '0.00044', commissionBPS: 0 }); 19 | fees.set('zora_testnet', { fee: '0.00044', commissionBPS: 0 }); 20 | return fees.get(network.name); 21 | } 22 | 23 | async function main() { 24 | const fees = getFees(); 25 | console.log(fees); 26 | const FeeManager = await deployContract('FeeManager', [ 27 | ethers.utils.parseEther(fees.fee), 28 | fees.commissionBPS, 29 | ]); 30 | 31 | console.log("FeeManager deployed to: ", FeeManager.address); 32 | } 33 | 34 | main().catch((error) => { 35 | console.error(error); 36 | process.exitCode = 1; 37 | }); 38 | -------------------------------------------------------------------------------- /scripts/deployZKEdition.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { deployZKEdition, theFuture } from "../core"; 3 | 4 | //TODO 5 | // set up DCNTSDK 6 | const DCNTSDK_ENDPOINT = ''; 7 | 8 | // set up ZKEditon 9 | const name = 'Token Name'; 10 | const symbol = 'TOKEN'; 11 | const hasAdjustableCap = false; 12 | const isSoulbound = false; 13 | const maxTokens = 25; 14 | const tokenPrice = ethers.utils.parseEther('0.01'); 15 | const maxTokenPurchase = 2; 16 | const presaleMerkleRoot = ethers.constants.HashZero; 17 | const presaleStart = theFuture.time(); 18 | const presaleEnd = theFuture.time(); 19 | const saleStart = theFuture.time(); 20 | const saleEnd = theFuture.time() + theFuture.oneMonth; 21 | const royaltyBPS = 10_00; 22 | const payoutAddress = ethers.constants.AddressZero; 23 | const contractURI = 'http://localhost/contract/'; 24 | const metadataURI = "http://localhost/metadata/"; 25 | const metadataRendererInit = { 26 | description: "This is the description for TOKEN.", 27 | imageURI: "http://localhost/image.jpg", 28 | animationURI: "http://localhost/song.mp3", 29 | }; 30 | const tokenGateConfig = { 31 | tokenAddress: ethers.constants.AddressZero, 32 | minBalance: 0, 33 | saleType: 0, 34 | } 35 | 36 | /** 37 | * TODO: 38 | * Set this to a deployed zkVerifier contract that can verify proofs 39 | * Default is zero address, can be updated by owner at any time 40 | */ 41 | const zkVerifier = ethers.constants.AddressZero; 42 | 43 | async function main() { 44 | const DCNTSDK = await ethers.getContractAt("DCNTSDK", DCNTSDK_ENDPOINT); 45 | const ZKEdition = await deployZKEdition( 46 | DCNTSDK, 47 | name, 48 | symbol, 49 | hasAdjustableCap, 50 | isSoulbound, 51 | maxTokens, 52 | tokenPrice, 53 | maxTokenPurchase, 54 | presaleMerkleRoot, 55 | presaleStart, 56 | presaleEnd, 57 | saleStart, 58 | saleEnd, 59 | royaltyBPS, 60 | payoutAddress, 61 | contractURI, 62 | metadataURI, 63 | metadataRendererInit, 64 | tokenGateConfig, 65 | zkVerifier 66 | ); 67 | console.log("ZKEdition deployed to: ", ZKEdition.address); 68 | } 69 | 70 | // We recommend this pattern to be able to use async/await everywhere 71 | // and properly handle errors. 72 | main().catch((error) => { 73 | console.error(error); 74 | process.exitCode = 1; 75 | }); 76 | -------------------------------------------------------------------------------- /scripts/upgradeSDK.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network } from "hardhat"; 2 | import { deployContract, getSDKAddresses } from "../core"; 3 | 4 | const GAS_INCREASE_BPS = 1_00; // 1% 5 | 6 | const UPGRADES = { 7 | DCNTSDK: false, 8 | DCNT721A: false, 9 | DCNT4907A: true, 10 | DCNTSeries: false, 11 | DCNTCrescendo: false, 12 | DCNTVault: false, 13 | DCNTStaking: false, 14 | DCNTMetadataRenderer: false, 15 | DCNTRegistry: false, 16 | DCNTVaultNFT: false, 17 | DCNTRentalMarket: false, 18 | ZKEdition: false, 19 | } 20 | 21 | const getOverrides = async () => { 22 | if ( network.name !== 'mainnet' && network.name !== 'goerli' && network.name !== 'sepolia' ) return {}; 23 | 24 | const increase = (num: ethers.BigNumber) => num.add(num.mul(GAS_INCREASE_BPS).div(10000)); 25 | 26 | const feeData = await ethers.provider.getFeeData(); 27 | const gasPrice = increase(feeData.gasPrice); 28 | const maxPriorityFeePerGas = increase(feeData.maxPriorityFeePerGas); 29 | const maxFeePerGas = gasPrice.add(maxPriorityFeePerGas); 30 | 31 | return { maxPriorityFeePerGas, maxFeePerGas }; 32 | } 33 | 34 | const getUpgradedContractAt = async (contract: string, address: string, args: any[] = []) => { 35 | let contractKey = contract as keyof typeof UPGRADES; 36 | if ( UPGRADES[contractKey] ) { 37 | console.log(`Deploying contract: ${contract}`); 38 | const overrides = await getOverrides(); 39 | return await deployContract(contract, args, overrides); 40 | } else { 41 | console.log(`Existing contract: ${contract}`); 42 | return await ethers.getContractAt(contract, address); 43 | } 44 | } 45 | 46 | async function main() { 47 | const [deployer] = await ethers.getSigners(); 48 | 49 | var addresses = getSDKAddresses(); 50 | 51 | const DCNT721A = await getUpgradedContractAt('DCNT721A', addresses.DCNT721A); 52 | const DCNT4907A = await getUpgradedContractAt('DCNT4907A', addresses.DCNT4907A); 53 | const DCNTSeries = await getUpgradedContractAt('DCNTSeries', addresses.DCNTSeries); 54 | const DCNTCrescendo = await getUpgradedContractAt('DCNTCrescendo', addresses.DCNTCrescendo); 55 | const DCNTVault = await getUpgradedContractAt('DCNTVault', addresses.DCNTVault); 56 | const DCNTStaking = await getUpgradedContractAt('DCNTStaking', addresses.DCNTStaking); 57 | const ZKEdition = await getUpgradedContractAt('ZKEdition', addresses?.ZKEdition); 58 | 59 | let DCNTMetadataRenderer; 60 | if ( UPGRADES.DCNTMetadataRenderer ) { 61 | const SharedNFTLogic = await deployContract('SharedNFTLogic'); 62 | DCNTMetadataRenderer = await deployContract('DCNTMetadataRenderer', [SharedNFTLogic.address]); 63 | } else { 64 | DCNTMetadataRenderer = await ethers.getContractAt('DCNTMetadataRenderer', addresses.DCNTMetadataRenderer); 65 | } 66 | 67 | const DCNTRegistry = await getUpgradedContractAt('DCNTRegistry', addresses.DCNTRegistry); 68 | 69 | const DCNTSDK = await getUpgradedContractAt('DCNTSDK', addresses.DCNTSDK, [ 70 | DCNT721A.address, 71 | DCNT4907A.address, 72 | DCNTSeries.address, 73 | DCNTCrescendo.address, 74 | DCNTVault.address, 75 | DCNTStaking.address, 76 | DCNTMetadataRenderer.address, 77 | DCNTRegistry.address, 78 | ZKEdition.address, 79 | ]); 80 | 81 | const DCNTVaultNFT = await getUpgradedContractAt('DCNTVaultNFT', addresses.DCNTVaultNFT, [DCNTSDK.address]); 82 | 83 | const DCNTRentalMarket = await getUpgradedContractAt('DCNTRentalMarket', addresses.DCNTRentalMarket); 84 | 85 | const contracts = { 86 | DCNTSDK: DCNTSDK.address, 87 | DCNT721A: DCNT721A.address, 88 | DCNT4907A: DCNT4907A.address, 89 | DCNTSeries: DCNTSeries.address, 90 | DCNTCrescendo: DCNTCrescendo.address, 91 | DCNTVault: DCNTVault.address, 92 | DCNTStaking: DCNTStaking.address, 93 | DCNTMetadataRenderer: DCNTMetadataRenderer.address, 94 | DCNTRegistry: DCNTRegistry.address, 95 | DCNTVaultNFT: DCNTVaultNFT.address, 96 | DCNTRentalMarket: DCNTRentalMarket.address, 97 | ZKEdition: ZKEdition.address, 98 | } 99 | 100 | console.log(JSON.stringify(contracts, null, 2),'\n'); 101 | } 102 | 103 | // We recommend this pattern to be able to use async/await everywhere 104 | // and properly handle errors. 105 | main().catch((error) => { 106 | console.error(error); 107 | process.exitCode = 1; 108 | }); 109 | -------------------------------------------------------------------------------- /scripts/verifyFeeManager.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network } from "hardhat"; 2 | import { getFees } from './deployFeeManager'; 3 | const { execSync } = require('child_process'); 4 | 5 | const run = (command: string) => { 6 | try { 7 | execSync(command, { stdio: 'inherit' }); 8 | } catch (error) { 9 | console.error(error); 10 | } 11 | } 12 | 13 | export const getFeeManager = () => { 14 | const feeManagers = new Map(); 15 | feeManagers.set('mainnet', "0x3B1d860AC03aa4FaAC49E1AEd2ca36cEb3dA506C"); 16 | feeManagers.set('polygon', "0x3B1d860AC03aa4FaAC49E1AEd2ca36cEb3dA506C"); 17 | feeManagers.set('optimism', "0x3B1d860AC03aa4FaAC49E1AEd2ca36cEb3dA506C"); 18 | feeManagers.set('arbitrum', "0x3B1d860AC03aa4FaAC49E1AEd2ca36cEb3dA506C"); 19 | feeManagers.set('base', "0x2983589C067B36078Ab65a603D9cE4BFba5e115c"); 20 | feeManagers.set('zora', "0x2983589C067B36078Ab65a603D9cE4BFba5e115c"); 21 | feeManagers.set('goerli', "0x97D80ff75F7bddeDb9663a3eC7C5492108466a59"); 22 | feeManagers.set('sepolia', "0x36C3a2b8550558fe7Eb86541DaFed469CACd2Ff9"); 23 | feeManagers.set('polygon_testnet', "0x97D80ff75F7bddeDb9663a3eC7C5492108466a59"); 24 | feeManagers.set('optimism_testnet', "0x97D80ff75F7bddeDb9663a3eC7C5492108466a59"); 25 | feeManagers.set('arbitrum_testnet', "0x97D80ff75F7bddeDb9663a3eC7C5492108466a59"); 26 | feeManagers.set('base_testnet', "0x2983589C067B36078Ab65a603D9cE4BFba5e115c"); 27 | feeManagers.set('zora_testnet', "0x36C3a2b8550558fe7Eb86541DaFed469CACd2Ff9"); 28 | return feeManagers.get(network.name); 29 | } 30 | 31 | async function main() { 32 | console.log('\nVerifying FeeManager...\n'); 33 | const { fee, commissionBPS } = getFees(); 34 | await run(`npx hardhat verify --network ${network.name} ${getFeeManager()} ${ethers.utils.parseEther(fee)} ${commissionBPS}`); 35 | } 36 | 37 | // We recommend this pattern to be able to use async/await everywhere 38 | // and properly handle errors. 39 | main().catch((error) => { 40 | console.error(error); 41 | process.exitCode = 1; 42 | }); 43 | -------------------------------------------------------------------------------- /scripts/verifySDK.ts: -------------------------------------------------------------------------------- 1 | import { ethers, network } from "hardhat"; 2 | import { getSDKAddresses } from "../core"; 3 | const { execSync } = require('child_process'); 4 | 5 | const run = (command: string) => { 6 | try { 7 | execSync(command, { stdio: 'inherit' }); 8 | } catch (error) { 9 | console.error(error); 10 | } 11 | } 12 | 13 | async function main() { 14 | const addresses = getSDKAddresses(); 15 | const DCNTSDK = await ethers.getContractAt('DCNTSDK', addresses.DCNTSDK); 16 | const DCNT721A = await ethers.getContractAt('DCNT721A', addresses.DCNT721A); 17 | const DCNT4907A = await ethers.getContractAt('DCNT4907A', addresses.DCNT4907A); 18 | const DCNTSeries = await ethers.getContractAt('DCNTSeries', addresses.DCNTSeries); 19 | const DCNTCrescendo = await ethers.getContractAt('DCNTCrescendo', addresses.DCNTCrescendo); 20 | const DCNTVault = await ethers.getContractAt('DCNTVault', addresses.DCNTVault); 21 | const DCNTStaking = await ethers.getContractAt('DCNTStaking', addresses.DCNTStaking); 22 | const DCNTMetadataRenderer = await ethers.getContractAt('DCNTMetadataRenderer', addresses.DCNTMetadataRenderer); 23 | const SharedNFTLogic = await ethers.getContractAt('DCNTMetadataRenderer', await DCNTMetadataRenderer.sharedNFTLogic()); 24 | const DCNTRegistry = await ethers.getContractAt('DCNTRegistry', addresses.DCNTRegistry); 25 | const DCNTVaultNFT = await ethers.getContractAt('DCNTVaultNFT', addresses.DCNTVaultNFT); 26 | const DCNTRentalMarket = await ethers.getContractAt('DCNTRentalMarket', addresses.DCNTRentalMarket); 27 | const ZKEdition = await ethers.getContractAt('ZKEdition', addresses.ZKEdition); 28 | 29 | console.log('\nVerifying DCNTSDK...\n'); 30 | run(`npx hardhat verify --network ${network.name} ` 31 | +`${DCNTSDK.address} ` 32 | +`${DCNT721A.address} ` 33 | +`${DCNT4907A.address} ` 34 | +`${DCNTSeries.address} ` 35 | +`${DCNTCrescendo.address} ` 36 | +`${DCNTVault.address} ` 37 | +`${DCNTStaking.address} ` 38 | +`${DCNTMetadataRenderer.address} ` 39 | +`${DCNTRegistry.address} ` 40 | +`${ZKEdition.address}` 41 | ); 42 | 43 | console.log('\nVerifying DCNT721A...\n'); 44 | await run(`npx hardhat verify --network ${network.name} ${DCNT721A.address}`); 45 | 46 | console.log('\nVerifying DCNT4907A...'); 47 | await run(`npx hardhat verify --network ${network.name} ${DCNT4907A.address}`); 48 | 49 | console.log('\nVerifying DCNTSeries...'); 50 | await run(`npx hardhat verify --network ${network.name} ${DCNTSeries.address}`); 51 | 52 | console.log('\nVerify DCNTCrescendo...\n'); 53 | await run(`npx hardhat verify --network ${network.name} ${DCNTCrescendo.address}`); 54 | 55 | console.log('\nVerifying DCNTVault...\n'); 56 | await run(`npx hardhat verify --network ${network.name} ${DCNTVault.address}`); 57 | 58 | console.log('\nVerifying DCNTStaking...\n'); 59 | await run(`npx hardhat verify --network ${network.name} ${DCNTStaking.address}`); 60 | 61 | console.log('\nVerifying DCNTRegistry...\n'); 62 | await run(`npx hardhat verify --network ${network.name} ${DCNTRegistry.address}`); 63 | 64 | console.log('\nVerifying SharedNFTLogic...\n'); 65 | await run(`npx hardhat verify --network ${network.name} ${SharedNFTLogic.address}`); 66 | 67 | console.log('\nVerifying DCNTMetadataRenderer...\n'); 68 | await run(`npx hardhat verify --network ${network.name} ${DCNTMetadataRenderer.address} ${SharedNFTLogic.address}`); 69 | 70 | console.log('\nVerifying DCNTVaultNFT...\n'); 71 | await run(`npx hardhat verify --network ${network.name} ${DCNTVaultNFT.address} ${DCNTSDK.address}`); 72 | 73 | console.log('\nVerifying DCNTRentalMarket...\n'); 74 | await run(`npx hardhat verify --network ${network.name} ${DCNTRentalMarket.address}\n`); 75 | 76 | console.log('\nVerifying ZKEdition...\n'); 77 | await run(`npx hardhat verify --network ${network.name} ${ZKEdition.address}\n`); 78 | } 79 | 80 | // We recommend this pattern to be able to use async/await everywhere 81 | // and properly handle errors. 82 | main().catch((error) => { 83 | console.error(error); 84 | process.exitCode = 1; 85 | }); 86 | -------------------------------------------------------------------------------- /tasks/index.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | task("balance", "Prints deployer's balance") 4 | .setAction(async (taskArgs, { ethers, network }) => { 5 | const [deployer] = await ethers.getSigners(); 6 | const balance = await ethers.provider.getBalance(deployer.address); 7 | console.log(`${network.name} ${ethers.utils.formatEther(balance)} ETH`); 8 | }); 9 | 10 | task("nonce", "Prints deployer's nonce") 11 | .setAction(async (taskArgs, { ethers, network }) => { 12 | const [deployer] = await ethers.getSigners(); 13 | const nonce = await ethers.provider.getTransactionCount(deployer.address); 14 | console.log(`${network.name} nonce: ${nonce}`); 15 | }); 16 | 17 | task("account", "Prints deployer's account") 18 | .setAction(async (taskArgs, { ethers, network }) => { 19 | const [deployer] = await ethers.getSigners(); 20 | const nonce = await ethers.provider.getTransactionCount(deployer.address); 21 | const balance = await ethers.provider.getBalance(deployer.address); 22 | console.log(`${network.name} nonce: ${nonce}`); 23 | console.log(`balance: ${ethers.utils.formatEther(balance)} ETH`); 24 | console.log(); 25 | }); 26 | 27 | task("gas", "Prints network fees") 28 | .setAction(async (taskArgs, { ethers, network }) => { 29 | const [feeData, pending, latest] = await Promise.all([ 30 | ethers.provider.getFeeData(), 31 | ethers.provider.getBlock('pending'), 32 | ethers.provider.getBlock('latest'), 33 | ]); 34 | const gasPrice = ethers.utils.formatUnits(feeData.gasPrice, 'gwei'); 35 | const maxFeePerGas = ethers.utils.formatUnits(feeData.maxFeePerGas, 'gwei'); 36 | const maxPriorityFeePerGas = ethers.utils.formatUnits(feeData.maxPriorityFeePerGas, 'gwei'); 37 | const pendingBaseFee = ethers.utils.formatUnits(pending.baseFeePerGas, 'gwei'); 38 | const latestBaseFee = ethers.utils.formatUnits(latest.baseFeePerGas, 'gwei'); 39 | console.log(`gas price: ${gasPrice}`); 40 | console.log(`max fee per gas: ${maxFeePerGas}`); 41 | console.log(`max priority fee per gas: ${maxPriorityFeePerGas}`); 42 | console.log(`pending base fee per gas: ${pendingBaseFee}`); 43 | console.log(`latest base fee per gas: ${latestBaseFee}`); 44 | }); 45 | 46 | task("transferOwnership", "Transfer ownership of a contract") 47 | .addParam("contractAddress", "Address of the ownable contract") 48 | .addParam("newOwner", "Address of the new owner") 49 | .setAction(async (taskArgs, { ethers, network }) => { 50 | const { contractAddress, newOwner } = taskArgs; 51 | const [signer] = await ethers.getSigners(); 52 | const abi = ['function transferOwnership(address)']; 53 | const contract = new ethers.Contract(contractAddress, abi, signer); 54 | await contract.transferOwnership(newOwner); 55 | console.log(`Ownership of contract ${contractAddress} transferred to ${newOwner}`); 56 | }); 57 | -------------------------------------------------------------------------------- /test/DCNT4907a.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers } from "hardhat"; 3 | import { before, beforeEach } from "mocha"; 4 | import { BigNumber, Contract } from "ethers"; 5 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 6 | import { deployDCNTSDK, deployDCNT4907A, theFuture, deployMockERC721 } from "../core"; 7 | 8 | const name = 'Decent'; 9 | const symbol = 'DCNT'; 10 | const hasAdjustableCap = false; 11 | const isSoulbound = false; 12 | const maxTokens = 4; 13 | const tokenPrice = ethers.utils.parseEther('0.01'); 14 | const maxTokenPurchase = 2; 15 | const presaleMerkleRoot = null; 16 | const presaleStart = theFuture.time(); 17 | const presaleEnd = theFuture.time(); 18 | const saleStart = theFuture.time(); 19 | const saleEnd = theFuture.time() + theFuture.oneYear; 20 | const royaltyBPS = 10_00; 21 | const feeManager = ethers.constants.AddressZero; 22 | const payoutAddress = ethers.constants.AddressZero; 23 | const contractURI = "http://localhost/contract/"; 24 | const metadataURI = "http://localhost/metadata/"; 25 | const metadataRendererInit = null; 26 | const tokenGateConfig = null; 27 | 28 | describe("DCNT4907A", async () => { 29 | let owner: SignerWithAddress, 30 | addr1: SignerWithAddress, 31 | addr2: SignerWithAddress, 32 | addr3: SignerWithAddress, 33 | addr4: SignerWithAddress, 34 | sdk: Contract, 35 | clone: Contract, 36 | nft: Contract, 37 | expiration: number, 38 | parentIP: Contract; 39 | 40 | before(async () => { 41 | [owner] = await ethers.getSigners(); 42 | sdk = await deployDCNTSDK(); 43 | parentIP = await deployMockERC721() 44 | clone = await deployDCNT4907A( 45 | sdk, 46 | name, 47 | symbol, 48 | hasAdjustableCap, 49 | isSoulbound, 50 | maxTokens, 51 | tokenPrice, 52 | maxTokenPurchase, 53 | presaleMerkleRoot, 54 | presaleStart, 55 | presaleEnd, 56 | saleStart, 57 | saleEnd, 58 | royaltyBPS, 59 | feeManager, 60 | payoutAddress, 61 | contractURI, 62 | metadataURI, 63 | metadataRendererInit, 64 | tokenGateConfig, 65 | parentIP.address 66 | ); 67 | }); 68 | 69 | describe("initialize()", async () => { 70 | it("should have the owner set as the EOA deploying the contract", async () => { 71 | expect(ethers.utils.getAddress(await clone.owner())).to.equal(owner.address); 72 | }); 73 | 74 | it("should initialize state which would otherwise be set in constructor", async () => { 75 | expect(await clone.name()).to.equal(name); 76 | expect(await clone.symbol()).to.equal(symbol); 77 | expect(await clone.MAX_TOKENS()).to.equal(maxTokens); 78 | expect(await clone.tokenPrice()).to.equal(tokenPrice); 79 | expect(await clone.maxTokenPurchase()).to.equal(maxTokenPurchase); 80 | expect(await clone.parentIP()).to.equal(parentIP.address); 81 | }); 82 | }); 83 | 84 | describe("setUser()", async () => { 85 | before(async () => { 86 | [addr1, addr2] = await ethers.getSigners(); 87 | await clone.mint(addr1.address, 1, { value: tokenPrice }); 88 | expiration = theFuture.time() + theFuture.oneDay; 89 | await clone.setUser(0, addr2.address, expiration); 90 | }); 91 | 92 | it("should set user to the intended account", async () => { 93 | expect(ethers.utils.getAddress(await clone.userOf(0))).to.equal(addr2.address); 94 | }); 95 | 96 | it("should set expiration to the intended timestamp", async () => { 97 | expect(await clone.userExpires(0)).to.equal(expiration); 98 | }); 99 | }); 100 | 101 | describe("userOf()", async () => { 102 | before(async () => { 103 | [addr1, addr2, addr3] = await ethers.getSigners(); 104 | await clone.setUser(0, addr3.address, expiration); 105 | }); 106 | 107 | it("should return the current user", async () => { 108 | expect(ethers.utils.getAddress(await clone.userOf(0))).to.equal(addr3.address); 109 | }); 110 | 111 | it("should return zero address after expiration", async () => { 112 | await theFuture.travel(theFuture.oneDay + 1); 113 | await theFuture.arrive(); 114 | expect(await clone.userOf(0)).to.equal(ethers.constants.AddressZero); 115 | }); 116 | }); 117 | 118 | describe("userExpires()", async () => { 119 | before(async () => { 120 | [addr1, addr2, addr3, addr4] = await ethers.getSigners(); 121 | expiration = theFuture.time() + theFuture.oneMonth; 122 | await clone.setUser(0, addr4.address, expiration); 123 | }); 124 | 125 | it("should return the expiration timestamp", async () => { 126 | expect(await clone.userExpires(0)).to.equal(expiration); 127 | }); 128 | }); 129 | 130 | describe("supportsInterface()", async () => { 131 | it('should support the interface for ERC721', async function () { 132 | expect(await clone.supportsInterface('0x80ac58cd')).to.eq(true); 133 | }); 134 | 135 | it('should support the interface for ERC4907', async function () { 136 | expect(await clone.supportsInterface('0xad092b5c')).to.eq(true); 137 | }); 138 | 139 | it('should support the interface for ERC2981', async function () { 140 | expect(await clone.supportsInterface('0x2a55205a')).to.eq(true); 141 | }); 142 | }); 143 | 144 | describe("royaltyInfo()", async () => { 145 | it('should calculate the royalty for the secondary sale', async function () { 146 | const royalty = await clone.royaltyInfo(0, tokenPrice); 147 | expect(royalty.royaltyAmount).to.eq(tokenPrice.div(10_000).mul(royaltyBPS)); 148 | }); 149 | 150 | it('should set owner as the receiver, unless there is a split', async function () { 151 | const ownerRoyalty = await clone.royaltyInfo(0, tokenPrice); 152 | expect(ownerRoyalty.receiver).to.eq(owner.address); 153 | 154 | // await nft.createSplit(...split); 155 | // const splitRoyalty = await nft.royaltyInfo(0, tokenPrice); 156 | // expect(splitRoyalty.receiver).to.eq(await nft.splitWallet()); 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /test/DCNTRegistry.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers } from "hardhat"; 3 | import { before, beforeEach } from "mocha"; 4 | import { BigNumber, Contract } from "ethers"; 5 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 6 | import { deployDCNTSDK, theFuture, deployMockERC721 } from "../core"; 7 | 8 | describe("DCNTRegistry", async () => { 9 | let owner: SignerWithAddress, 10 | sdk: Contract, 11 | registry: Contract, 12 | nft: Contract; 13 | 14 | describe("constructor()", async () => { 15 | before(async () => { 16 | [owner] = await ethers.getSigners(); 17 | sdk = await deployDCNTSDK(); 18 | registry = await ethers.getContractAt('DCNTRegistry', await sdk.contractRegistry()); 19 | }); 20 | 21 | it("should result in a properly deployed contract", async () => { 22 | expect(registry.address).to.be.properAddress; 23 | }); 24 | }); 25 | 26 | describe("register()", async () => { 27 | it("should register a deployment for a specific deployer", async () => { 28 | nft = await deployMockERC721(); 29 | const registerTx = await registry.register(owner.address, nft.address, ''); 30 | const deployments = await registry.query(owner.address); 31 | expect(deployments[0]).to.equal(nft.address); 32 | }); 33 | 34 | it("should emit an event with the name of the contract", async () => { 35 | nft = await deployMockERC721(); 36 | const registerTx = await registry.register(owner.address, nft.address, 'MockERC721'); 37 | 38 | const receipt = await registerTx.wait(); 39 | const event = receipt.events.find((x: any) => x.event === 'Register').args; 40 | 41 | expect(event.deployer).to.equal(owner.address); 42 | expect(event.deployment).to.equal(nft.address); 43 | expect(event.key).to.equal('MockERC721'); 44 | }); 45 | }); 46 | 47 | describe("remove()", async () => { 48 | it("should remove a deployment for a specific deployer", async () => { 49 | const before = await registry.query(owner.address); 50 | expect(before.length).to.equal(2); 51 | 52 | const removeTx = await registry.remove(owner.address, nft.address); 53 | const after = await registry.query(owner.address); 54 | expect(after.length).to.equal(1); 55 | }); 56 | }); 57 | 58 | describe("query()", async () => { 59 | it("should return all deployments for a specific deployer", async () => { 60 | nft = await deployMockERC721(); 61 | const registerTx = await registry.register(owner.address, nft.address, ''); 62 | const deployments = await registry.query(owner.address); 63 | expect(deployments.length).to.equal(2); 64 | expect(deployments[1]).to.equal(nft.address); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/DCNTStaking.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers } from "hardhat"; 3 | import { before, beforeEach } from "mocha"; 4 | import { BigNumber, Contract } from "ethers"; 5 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 6 | import { deployDCNTSDK, deployDCNTStaking, deployMockERC20, deployMockERC721, theFuture } from "../core"; 7 | 8 | const tokenDecimals = 18; 9 | const vaultDuration = 100; // days 10 | 11 | // amounts 12 | const oneEth = ethers.utils.parseEther("1"); 13 | const vaultFund = oneEth.mul(100); 14 | 15 | describe("DCNTStaking", async () => { 16 | let owner: SignerWithAddress, 17 | addr1: SignerWithAddress, 18 | addr2: SignerWithAddress, 19 | addr3: SignerWithAddress, 20 | addr4: SignerWithAddress, 21 | sdk: Contract, 22 | clone: Contract, 23 | token: Contract, 24 | nft: Contract, 25 | totalSupply: number; 26 | 27 | before(async () => { 28 | [owner] = await ethers.getSigners(); 29 | sdk = await deployDCNTSDK(); 30 | token = await deployMockERC20(vaultFund); 31 | nft = await deployMockERC721(); 32 | totalSupply = 10; 33 | await theFuture.travel(theFuture.oneDay); 34 | clone = await deployDCNTStaking( 35 | sdk, 36 | nft.address, 37 | token.address, 38 | vaultDuration, 39 | totalSupply 40 | ); 41 | }); 42 | 43 | describe("initialize()", async () => { 44 | it("should have the owner set as the EOA deploying the contract", async () => { 45 | expect(ethers.utils.getAddress(await clone.owner())).to.equal(owner.address); 46 | }); 47 | 48 | it("should initialize state which would otherwise be set in constructor", async () => { 49 | expect(await clone.nftAddress()).to.equal(nft.address); 50 | expect(await clone.erc20Address()).to.equal(token.address); 51 | }); 52 | 53 | it("should calculate vault start and end dates based on block timestamp", async () => { 54 | expect(await clone.vaultStart()).to.equal(theFuture.time()); 55 | expect(await clone.vaultEnd()).to.equal(theFuture.time() + (theFuture.oneDay * vaultDuration)); 56 | }); 57 | }); 58 | 59 | describe("stake()", async () => { 60 | it("should revert if not approved for token or not an approved operator", async () => { 61 | const stakedNFT = await deployMockERC721(); 62 | await stakedNFT.mintNft(10); 63 | 64 | const staking = await deployDCNTStaking( 65 | sdk, 66 | stakedNFT.address, 67 | token.address, 68 | vaultDuration, 69 | totalSupply 70 | ); 71 | 72 | await expect(staking.stake([0])).to.be.revertedWith('not approved for transfer'); 73 | await stakedNFT.approve(staking.address, 0); 74 | await staking.stake([0]); 75 | expect(await staking.balanceOf(owner.address)).to.equal(1); 76 | 77 | await expect(staking.stake([1,2,3,4,5])).to.be.revertedWith('not approved for transfer'); 78 | await stakedNFT.setApprovalForAll(staking.address, true); 79 | await staking.stake([1,2,3,4,5]); 80 | expect(await staking.balanceOf(owner.address)).to.equal(6); 81 | }); 82 | }); 83 | 84 | describe("vault functionality", async () => { 85 | it("should have a vault balance of zero", async () => { 86 | expect(await nft.balanceOf(owner.address)).to.equal(0); 87 | }) 88 | 89 | describe("and 50 tokens are added to the vault", async () => { 90 | it("should have a vault balance of 50", async () => { 91 | await token.connect(owner).transfer(clone.address, 50); 92 | expect(await token.balanceOf(clone.address)).to.equal(50); 93 | }) 94 | }); 95 | 96 | describe("and 50 more tokens are added to the vault", async () => { 97 | it("should have a vault balance of 100", async () => { 98 | await token.connect(owner).transfer(clone.address, 50); 99 | expect(await token.balanceOf(clone.address)).to.equal(100); 100 | }) 101 | }); 102 | }); 103 | 104 | describe("claiming functionality", async () => { 105 | let owner: SignerWithAddress, 106 | addr1: SignerWithAddress, 107 | addr2: SignerWithAddress, 108 | addr3: SignerWithAddress, 109 | addr4: SignerWithAddress, 110 | sdk: Contract, 111 | clone: Contract, 112 | token: Contract, 113 | nft: Contract, 114 | totalSupply: number; 115 | 116 | before(async () => { 117 | [addr1, addr2, addr3, addr4] = await ethers.getSigners(); 118 | sdk = await deployDCNTSDK(); 119 | token = await deployMockERC20(vaultFund.mul(2)); 120 | nft = await deployMockERC721(); 121 | totalSupply = 10; 122 | await theFuture.travel(theFuture.oneDay); 123 | clone = await deployDCNTStaking( 124 | sdk, 125 | nft.address, 126 | token.address, 127 | vaultDuration, 128 | totalSupply 129 | ); 130 | 131 | // fund the vault 132 | await token.connect(addr1).transfer(clone.address, vaultFund); 133 | 134 | // mint 1/10 of nft collection to owner 135 | await nft.connect(addr1).mintNft(1); 136 | await nft.connect(addr2).mintNft(8); 137 | await nft.connect(addr3).mintNft(1); 138 | 139 | // approve transfer and stake 1 nft 140 | await nft.connect(addr1).approve(clone.address, 0); 141 | await theFuture.travel(theFuture.oneDay); 142 | await clone.connect(addr1).stake([0]); 143 | 144 | // approve transfer and stake 1 nft 145 | await nft.connect(addr2).approve(clone.address, 1); 146 | await clone.connect(addr2).stake([1]); 147 | }); 148 | 149 | describe("and a user with one nft stakes for 1 day", async () => { 150 | it("they would have earned 0.1 token", async () => { 151 | await theFuture.travel(theFuture.oneDay); 152 | await theFuture.arrive(); 153 | const earn = await clone.earningInfo(addr1.address, [0]); 154 | expect(earn).to.equal(ethers.utils.parseEther('0.1')); 155 | }); 156 | }); 157 | 158 | describe("and continues to stake for another 1.5 days", async () => { 159 | it("they would have earned 0.25 token", async () => { 160 | await theFuture.travel(theFuture.oneDay*1.5); 161 | await theFuture.arrive(); 162 | const earn = await clone.earningInfo(addr1.address, [0]); 163 | expect(earn).to.equal(ethers.utils.parseEther('0.25')); 164 | }); 165 | }); 166 | 167 | describe("and continues to stake for another 2.5 days", async () => { 168 | it("they would have earned 0.5 token", async () => { 169 | await theFuture.travel(theFuture.oneDay*2.5); 170 | await theFuture.arrive(); 171 | const earn = await clone.earningInfo(addr1.address, [0]); 172 | expect(earn).to.equal(ethers.utils.parseEther('0.5')); 173 | }); 174 | }); 175 | 176 | describe("and continues to stake for another day after a user claims", async () => { 177 | it("they would have earned 0.6 token", async () => { 178 | await clone.connect(addr2).claim([1]); 179 | 180 | await theFuture.travel(theFuture.oneDay); 181 | await theFuture.arrive(); 182 | const earn = await clone.earningInfo(addr1.address, [0]); 183 | expect(earn).to.equal(ethers.utils.parseEther('0.6')); 184 | }); 185 | }); 186 | 187 | describe("and stakes for 7 days total and the vault fund is doubled", async () => { 188 | it("they would have earned 1.4 token", async () => { 189 | await token.connect(addr1).transfer(clone.address, vaultFund); 190 | 191 | await theFuture.travel(theFuture.oneDay); 192 | await theFuture.arrive(); 193 | const earn = await clone.earningInfo(addr1.address, [0]); 194 | expect(earn).to.equal(ethers.utils.parseEther('1.4')); 195 | }); 196 | }); 197 | 198 | describe("and then tries to stake the same nft again ", async () => { 199 | it("should revert with already staked", async () => { 200 | await expect(clone.connect(addr1).stake([0])).to.be.revertedWith("already staked"); 201 | }); 202 | }); 203 | 204 | describe("and then tries to stake another user's nft ", async () => { 205 | it("should revert with not your token", async () => { 206 | await expect(clone.connect(addr1).stake([2])).to.be.revertedWith("not your token"); 207 | }); 208 | }); 209 | 210 | describe("and another user tries to stake without approving first", async () => { 211 | it("should revert with not approved for transfer", async () => { 212 | await expect(clone.connect(addr3).stake([9])).to.be.revertedWith("not approved for transfer"); 213 | }); 214 | }); 215 | }); 216 | }); 217 | 218 | -------------------------------------------------------------------------------- /test/DCNTVault.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers } from "hardhat"; 3 | import { before, beforeEach } from "mocha"; 4 | import { BigNumber, Contract } from "ethers"; 5 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 6 | import { deployDCNTSDK, deployDCNTVault, deployMockERC20, deployMockERC721, theFuture } from "../core"; 7 | 8 | describe("DCNTVault", async () => { 9 | let owner: SignerWithAddress, 10 | sdk: Contract, 11 | clone: Contract, 12 | token: Contract, 13 | nft: Contract; 14 | 15 | let addr1: SignerWithAddress, 16 | addr2: SignerWithAddress, 17 | addr3: SignerWithAddress, 18 | addr4: SignerWithAddress, 19 | vault: Contract, 20 | unlockedVault: Contract; 21 | 22 | describe("initialize()", async () => { 23 | before(async () => { 24 | [owner] = await ethers.getSigners(); 25 | sdk = await deployDCNTSDK(); 26 | nft = await deployMockERC721(); 27 | token = await deployMockERC20(100); 28 | clone = await deployDCNTVault( 29 | sdk, 30 | token.address, 31 | nft.address, 32 | 100, 33 | theFuture.time() 34 | ); 35 | }); 36 | 37 | it("should have the owner set as the EOA deploying the contract", async () => { 38 | expect(ethers.utils.getAddress(await clone.owner())).to.equal(owner.address); 39 | }); 40 | 41 | it("should initialize state which would otherwise be set in constructor", async () => { 42 | expect(ethers.utils.getAddress(await clone.vaultDistributionToken())).to.equal(token.address); 43 | expect(ethers.utils.getAddress(await clone.nftVaultKey())).to.equal(nft.address); 44 | expect(await clone.unlockDate()).to.equal(theFuture.time()); 45 | }); 46 | }); 47 | 48 | 49 | describe("vault functionality", async () => { 50 | beforeEach(async () => { 51 | [addr1, addr2, addr3, addr4] = await ethers.getSigners(); 52 | sdk = await deployDCNTSDK(); 53 | nft = await deployMockERC721(); 54 | token = await deployMockERC20(100); 55 | vault = await deployDCNTVault( 56 | sdk, 57 | token.address, 58 | nft.address, 59 | 100, 60 | theFuture.time() 61 | ); 62 | }) 63 | 64 | it("should have a vault balance of zero", async () => { 65 | expect(await vault.vaultBalance()).to.equal(0); 66 | }); 67 | 68 | describe("and 50 tokens are added to the vault", async () => { 69 | it("should have a vault balance of 50", async () => { 70 | await token.connect(addr1).transfer(vault.address, 50); 71 | expect(await vault.vaultBalance()).to.equal(50); 72 | }); 73 | }); 74 | 75 | describe("and 50 more tokens are added to the vault", async () => { 76 | it("should have a vault balance of 100", async () => { 77 | await token.connect(addr1).transfer(vault.address, 100); 78 | expect(await vault.vaultBalance()).to.equal(100); 79 | }); 80 | }); 81 | }); 82 | 83 | describe("claiming core functionality", async () => { 84 | before(async () => { 85 | [addr1, addr2, addr3, addr4] = await ethers.getSigners(); 86 | nft = await deployMockERC721(); 87 | token = await deployMockERC20(100); 88 | let tomorrow = theFuture.time() + theFuture.oneDay; 89 | 90 | // mint 1 nft for 1 address and 2 for 2 more 91 | await nft.connect(addr1).mintNft(1); 92 | await nft.connect(addr2).mintNft(2); 93 | await nft.connect(addr3).mintNft(2); 94 | 95 | vault = await deployDCNTVault( 96 | sdk, 97 | token.address, 98 | nft.address, 99 | 100, 100 | tomorrow 101 | ); 102 | 103 | // send 100 tokens to the vault 104 | await token.connect(addr1).transfer(vault.address, 100); 105 | }); 106 | 107 | describe("and the vault is locked", async () => { 108 | describe("and a user with an nft tries to pull out money", async () => { 109 | it("should produce a warning preventing this", async () => { 110 | await expect(vault.claim(addr1.address, 0)).to.be.revertedWith( 111 | 'vault is still locked' 112 | ); 113 | }); 114 | }); 115 | 116 | describe("and a user without an nft tries to pull out money", async () => { 117 | it("should produce a warning preventing this", async () => { 118 | await expect(vault.claim(addr4.address, 5)).to.be.revertedWith( 119 | 'vault is still locked' 120 | ); 121 | }); 122 | }); 123 | }); 124 | 125 | before(async () => { 126 | [addr1, addr2, addr3, addr4] = await ethers.getSigners(); 127 | nft = await deployMockERC721(); 128 | token = await deployMockERC20(100); 129 | let yesterday = theFuture.time() - theFuture.oneDay; 130 | 131 | // set nft portions 132 | await nft.connect(addr1).mintNft(1); 133 | await nft.connect(addr2).mintNft(2); 134 | await nft.connect(addr3).mintNft(2); 135 | 136 | unlockedVault = await deployDCNTVault( 137 | sdk, 138 | token.address, 139 | nft.address, 140 | 5, 141 | yesterday 142 | ); 143 | 144 | // send 100 tokens to the vault 145 | await token.connect(addr1).transfer(unlockedVault.address, 100); 146 | }); 147 | 148 | describe("and the vault is unlocked", async () => { 149 | describe("and a user without any nft keys tries to redeem tokens", async () => { 150 | it("he would recieve zero tokens", async () => { 151 | await expect(unlockedVault.claim(addr4.address, 0)).to.be.revertedWith( 152 | 'address does not own token' 153 | ); 154 | }); 155 | }); 156 | 157 | describe("and a user with one nft tries to redeem his tokens (1/5 nfts * 100 tokens)", async () => { 158 | it("should transfer 20 tokens to the user's account", async () => { 159 | await unlockedVault.claim(addr1.address, 0); 160 | expect(await token.balanceOf(addr1.address)).to.equal(20); 161 | }); 162 | }); 163 | 164 | describe("and a user who has already redeemed his tokens tries to redeem again", async () => { 165 | it("should prevent the user from doing this", async () => { 166 | await expect(unlockedVault.claim(addr1.address, 0)).to.be.revertedWith( 167 | 'token already claimed' 168 | ); 169 | }); 170 | }); 171 | 172 | describe("and a user with two nfts tries to redeem tokens (2/5 * 100)", async () => { 173 | it("should should transfer 40 tokens to the user's account", async () => { 174 | await unlockedVault.claimMany(addr2.address, [1, 2]); 175 | // balance will equal 40 176 | expect(await token.balanceOf(addr2.address)).to.equal(40); 177 | }); 178 | }); 179 | }); 180 | }); 181 | 182 | describe("claiming division tests", async () => { 183 | before(async () => { 184 | [addr1, addr2, addr3, addr4] = await ethers.getSigners(); 185 | nft = await deployMockERC721(); 186 | token = await deployMockERC20(73); 187 | let yesterday = theFuture.time() - theFuture.oneDay; 188 | 189 | await nft.connect(addr1).mintNft(3); 190 | await nft.connect(addr2).mintNft(1); 191 | await nft.connect(addr3).mintNft(1); 192 | await nft.connect(addr4).mintNft(6); 193 | 194 | unlockedVault = await deployDCNTVault( 195 | sdk, 196 | token.address, 197 | nft.address, 198 | 11, 199 | yesterday 200 | ); 201 | await token.connect(addr1).transfer(unlockedVault.address, 73); 202 | }); 203 | 204 | describe("and a user with three of eleven nfts tries to redeem tokens (3 * 73)/11", async () => { 205 | it("should should transfer 19 tokens to the user's account", async () => { 206 | await unlockedVault.claimMany(addr1.address, [0, 1, 2]); 207 | expect(await token.balanceOf(addr1.address)).to.equal(19); 208 | }); 209 | }); 210 | 211 | describe("and he then receives another one and tries to redeem it", async () => { 212 | it("should should transfer 6 tokens to the user's account (1 * 73)/11", async () => { 213 | await nft.connect(addr2)["safeTransferFrom(address,address,uint256)"](addr2.address, addr1.address, 3); 214 | await unlockedVault.claim(addr1.address, 3); 215 | expect(await token.balanceOf(addr1.address)).to.equal(25); 216 | }); 217 | }); 218 | 219 | describe("and he then receives another one thats already been claimed and tries to redeem it", async () => { 220 | it("should return an error", async () => { 221 | await unlockedVault.claim(addr3.address, 4); 222 | await nft.connect(addr3)["safeTransferFrom(address,address,uint256)"](addr3.address, addr1.address, 4); 223 | await expect(unlockedVault.claim(addr1.address, 4)).to.be.revertedWith( 224 | 'token already claimed' 225 | ); 226 | }); 227 | }); 228 | 229 | describe("and a user tries to claim an already claimed token", async () => { 230 | it("should revert with token already claimed", async () => { 231 | await expect(unlockedVault.claim(addr1.address, 1)).to.be.revertedWith( 232 | "token already claimed" 233 | ); 234 | }); 235 | }); 236 | 237 | describe("and a user tries to claim one token using claim", async () => { 238 | it("should show user w balance of 1/11 * 73 tokens (~6)", async () => { 239 | await unlockedVault.claim(addr4.address, 6); 240 | expect(await token.balanceOf(addr4.address)).to.equal(6); 241 | }); 242 | }); 243 | }); 244 | }); 245 | -------------------------------------------------------------------------------- /test/DCNTVaultNFT.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers } from "hardhat"; 3 | import { before, beforeEach } from "mocha"; 4 | import { BigNumber, Contract } from "ethers"; 5 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 6 | import { deployDCNTSDK, deployDCNTVaultNFT, DCNTVaultNFTCreate, theFuture } from "../core"; 7 | 8 | const name = 'Decent'; 9 | const symbol = 'DCNT'; 10 | const hasAdjustableCap = false; 11 | const isSoulbound = false; 12 | const maxTokens = 4; 13 | const tokenPrice = ethers.utils.parseEther('0.01'); 14 | const maxTokenPurchase = 2; 15 | const presaleMerkleRoot = null; 16 | const presaleStart = theFuture.time(); 17 | const presaleEnd = theFuture.time(); 18 | const saleStart = theFuture.time(); 19 | const saleEnd = theFuture.time() + theFuture.oneYear; 20 | const royaltyBPS = 10_00; 21 | const feeManager = ethers.constants.AddressZero; 22 | const payoutAddress = ethers.constants.AddressZero; 23 | const contractURI = 'http://localhost/contract/'; 24 | const metadataURI = 'http://localhost/metadata/'; 25 | const metadataRendererInit = null; 26 | const tokenGateConfig = null; 27 | const vaultDistributionTokenAddress = ethers.constants.AddressZero; 28 | const unlockDate = theFuture.time(); 29 | 30 | describe("DCNTVaultNFT", async () => { 31 | let owner: SignerWithAddress, 32 | sdk: Contract, 33 | registry: Contract, 34 | dcntVaultNFT: Contract, 35 | nft: Contract, 36 | vault: Contract; 37 | 38 | before(async () => { 39 | [owner] = await ethers.getSigners(); 40 | sdk = await deployDCNTSDK(); 41 | registry = await ethers.getContractAt('DCNTRegistry', sdk.contractRegistry()); 42 | dcntVaultNFT = await deployDCNTVaultNFT(sdk); 43 | }); 44 | 45 | describe("constructor()", async () => { 46 | it("should result in a properly deployed contract", async () => { 47 | expect(dcntVaultNFT.address).to.be.properAddress; 48 | }); 49 | }); 50 | 51 | describe("create()", async () => { 52 | before(async () => { 53 | let supports4907 = false; 54 | [nft, vault] = await DCNTVaultNFTCreate( 55 | dcntVaultNFT, 56 | sdk, 57 | name, 58 | symbol, 59 | hasAdjustableCap, 60 | isSoulbound, 61 | maxTokens, 62 | tokenPrice, 63 | maxTokenPurchase, 64 | presaleMerkleRoot, 65 | presaleStart, 66 | presaleEnd, 67 | saleStart, 68 | saleEnd, 69 | royaltyBPS, 70 | feeManager, 71 | payoutAddress, 72 | contractURI, 73 | metadataURI, 74 | metadataRendererInit, 75 | tokenGateConfig, 76 | vaultDistributionTokenAddress, 77 | unlockDate, 78 | supports4907 79 | ); 80 | }); 81 | 82 | it("should deploy and initialize a DCNT721A contract", async () => { 83 | expect(nft.address).to.be.properAddress; 84 | expect(await nft.supportsInterface('0xad092b5c')).to.eq(false); 85 | }); 86 | 87 | it("should have the owner properly set on the DCNT721A deployment", async () => { 88 | expect(ethers.utils.getAddress(await nft.owner())).to.equal(owner.address); 89 | }); 90 | 91 | it("should deploy and initialize a DCNTVault contract", async () => { 92 | expect(vault.address).to.be.properAddress; 93 | }); 94 | 95 | it("should have the owner properly set on the DCNTVault deployment", async () => { 96 | expect(ethers.utils.getAddress(await vault.owner())).to.equal(owner.address); 97 | }); 98 | 99 | it("should register the nft in the sdk contract registry", async () => { 100 | const [registeredNFT, registeredVault] = await registry.query(owner.address); 101 | expect(registeredNFT).to.equal(nft.address); 102 | }); 103 | 104 | it("should register the vault in the sdk contract registry", async () => { 105 | const [registeredNFT, registeredVault] = await registry.query(owner.address); 106 | expect(registeredVault).to.equal(vault.address); 107 | }); 108 | 109 | it("should optionally deploy and initialize a DCNT4907A contract", async () => { 110 | let supports4907 = true; 111 | [nft, vault] = await DCNTVaultNFTCreate( 112 | dcntVaultNFT, 113 | sdk, 114 | name, 115 | symbol, 116 | hasAdjustableCap, 117 | isSoulbound, 118 | maxTokens, 119 | tokenPrice, 120 | maxTokenPurchase, 121 | presaleMerkleRoot, 122 | presaleStart, 123 | presaleEnd, 124 | saleStart, 125 | saleEnd, 126 | royaltyBPS, 127 | feeManager, 128 | payoutAddress, 129 | contractURI, 130 | metadataURI, 131 | metadataRendererInit, 132 | tokenGateConfig, 133 | vaultDistributionTokenAddress, 134 | unlockDate, 135 | supports4907 136 | ); 137 | 138 | expect(nft.address).to.be.properAddress; 139 | expect(await nft.supportsInterface('0xad092b5c')).to.eq(true); 140 | }); 141 | 142 | it("should have the owner properly set on the DCNT4907A deployment", async () => { 143 | expect(ethers.utils.getAddress(await nft.owner())).to.equal(owner.address); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /test/FeeManager.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers } from "hardhat"; 3 | import { before, beforeEach } from "mocha"; 4 | import { BigNumber, Contract } from "ethers"; 5 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 6 | import { deployDCNTSDK, deployDCNTSeries, deployMockERC721, deployContract, theFuture, sortByAddress, base64decode } from "../core"; 7 | import { MerkleTree } from "merkletreejs"; 8 | const keccak256 = require("keccak256"); 9 | 10 | const name = 'Decent'; 11 | const symbol = 'DCNT'; 12 | const hasAdjustableCap = true; 13 | const isSoulbound = false; 14 | const startTokenId = 0; 15 | const endTokenId = 0; 16 | const maxTokens = 4; 17 | const tokenPrice = ethers.utils.parseEther('0.01'); 18 | const maxTokensPerOwner = 2; 19 | const presaleMerkleRoot = null; 20 | const presaleStart = theFuture.time(); 21 | const presaleEnd = theFuture.time(); 22 | let saleStart = theFuture.time(); 23 | let saleEnd = theFuture.time() + theFuture.oneYear; 24 | const royaltyBPS = 10_00; 25 | const feeManager = ethers.constants.AddressZero; 26 | const payoutAddress = ethers.constants.AddressZero; 27 | const currencyOracle = ethers.constants.AddressZero; 28 | const contractURI = "http://localhost/contract/"; 29 | const metadataURI = "http://localhost/metadata/"; 30 | const tokenGateConfig = { 31 | tokenAddress: ethers.constants.AddressZero, 32 | minBalance: 0, 33 | saleType: 0, 34 | } 35 | 36 | describe("FeeManager", async () => { 37 | let owner: SignerWithAddress, 38 | addr1: SignerWithAddress, 39 | addr2: SignerWithAddress, 40 | addr3: SignerWithAddress, 41 | addr4: SignerWithAddress, 42 | sdk: Contract, 43 | feeManager: Contract, 44 | splitMain: Contract, 45 | nft: Contract, 46 | fixedFee: BigNumber, 47 | commissionBPS: BigNumber, 48 | split: any[]; 49 | 50 | before(async () => { 51 | sdk = await deployDCNTSDK(); 52 | [addr1, addr2, addr3, addr4] = await ethers.getSigners(); 53 | await theFuture.reset(); 54 | saleStart = theFuture.time(); 55 | saleEnd = saleStart + theFuture.oneYear; 56 | }); 57 | 58 | describe("constructor()", async () => { 59 | it("should set the initial parameters for a fee manager", async () => { 60 | fixedFee = ethers.utils.parseEther('0.0005'); // $1.00 USD in ETH at $2000 USD 61 | commissionBPS = 10_00; // 10% in BPS 62 | feeManager = await deployContract('FeeManager',[fixedFee, commissionBPS]); 63 | expect(await feeManager.fee()).to.equal(fixedFee); 64 | expect(await feeManager.commissionBPS()).to.equal(commissionBPS); 65 | }); 66 | }); 67 | 68 | describe("setFees()", async () => { 69 | it("should update the fees for a fee manager", async () => { 70 | fixedFee = ethers.utils.parseEther('0.0010'); // $2.00 USD in ETH at $2000 USD 71 | commissionBPS = 5_00; // 5% in BPS 72 | await feeManager.setFees(fixedFee, commissionBPS); 73 | expect(await feeManager.fee()).to.equal(fixedFee); 74 | expect(await feeManager.commissionBPS()).to.equal(commissionBPS); 75 | }); 76 | }); 77 | 78 | describe("calculateFee()", async () => { 79 | it("should return the caluclated fee for the specified price and quantity", async () => { 80 | let [fee] = await feeManager.calculateFees(tokenPrice, 1); 81 | expect(fee).to.equal(fixedFee); 82 | [fee] = await feeManager.calculateFees(tokenPrice, 5); 83 | expect(fee).to.equal(fixedFee.mul(5)); 84 | }); 85 | }); 86 | 87 | describe("calculateCommission()", async () => { 88 | it("should return the calculated commission for the specified price and quantity", async () => { 89 | let [, commission] = await feeManager.calculateFees(tokenPrice, 1) 90 | expect(commission).to.equal(tokenPrice.mul(commissionBPS).div(100_00)); 91 | [, commission] = await feeManager.calculateFees(tokenPrice, 5) 92 | expect(commission).to.equal(tokenPrice.mul(commissionBPS).div(100_00).mul(5)); 93 | }); 94 | }); 95 | 96 | describe("withdraw()", async () => { 97 | it("should allow withdrawals by owner", async () => { 98 | const fixedFee = ethers.utils.parseEther('0.0005'); // $1.00 USD in ETH at $2000 USD 99 | const commissionBPS = 10_00; // 10% in BPS 100 | const feeManager = await deployContract('FeeManager',[fixedFee, commissionBPS]); 101 | 102 | const freshNFT = await deployDCNTSeries( 103 | sdk, 104 | name, 105 | symbol, 106 | hasAdjustableCap, 107 | isSoulbound, 108 | startTokenId, 109 | endTokenId, 110 | royaltyBPS, 111 | feeManager.address, 112 | payoutAddress, 113 | currencyOracle, 114 | contractURI, 115 | metadataURI, 116 | { 117 | maxTokens, 118 | tokenPrice, 119 | maxTokensPerOwner, 120 | presaleMerkleRoot, 121 | presaleStart, 122 | presaleEnd, 123 | saleStart, 124 | saleEnd, 125 | tokenGateConfig 126 | } 127 | ); 128 | 129 | await freshNFT.mint(0, addr1.address, 1, { value: tokenPrice.add(fixedFee) }); 130 | const commission = tokenPrice.mul(commissionBPS).div(100_00); 131 | const totalFees = commission.add(fixedFee); 132 | const before = await addr1.getBalance(); 133 | const tx = await feeManager.withdraw(); 134 | const receipt = await tx.wait(); 135 | const gas = receipt.cumulativeGasUsed * receipt.effectiveGasPrice; 136 | const after = await addr1.getBalance(); 137 | const withdrawn = after.sub(before).add(gas); 138 | expect(withdrawn).to.equal(totalFees); 139 | }); 140 | 141 | it("should revert if called by an account which is not the owner", async () => { 142 | await expect( 143 | feeManager.connect(addr2).withdraw() 144 | ).to.be.revertedWith('Ownable: caller is not the owner'); 145 | }); 146 | }); 147 | 148 | describe("createSplit()", async () => { 149 | before(async () => { 150 | const payouts = sortByAddress([ 151 | { 152 | address: addr2.address, 153 | percent: (1_000_000 / 100) * 90 154 | }, 155 | { 156 | address: addr3.address, 157 | percent: (1_000_000 / 100) * 10 158 | }, 159 | ]); 160 | 161 | const addresses = payouts.map(payout => payout.address); 162 | const percents = payouts.map(payout => payout.percent); 163 | const distributorFee = 0; 164 | split = [addresses, percents, distributorFee]; 165 | splitMain = await deployContract('SplitMain'); 166 | }); 167 | 168 | it("should create a split", async () => { 169 | expect(await feeManager.splitWallet()).to.equal(ethers.constants.AddressZero); 170 | await feeManager.createSplit(splitMain.address, ...split); 171 | expect(await feeManager.splitWallet()).to.not.equal(ethers.constants.AddressZero); 172 | }); 173 | }); 174 | 175 | }); 176 | -------------------------------------------------------------------------------- /test/Splits.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers } from "hardhat"; 3 | import { before, beforeEach } from "mocha"; 4 | import { BigNumber, Contract } from "ethers"; 5 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 6 | import { deployDCNTSDK, deployDCNT4907A, deployContract, theFuture, deployMockERC20, sortByAddress } from "../core"; 7 | 8 | const name = 'Decent'; 9 | const symbol = 'DCNT'; 10 | const hasAdjustableCap = false; 11 | const isSoulbound = false; 12 | const maxTokens = 4; 13 | const tokenPrice = ethers.utils.parseEther('1'); 14 | const maxTokenPurchase = 2; 15 | const presaleMerkleRoot = null; 16 | const presaleStart = theFuture.time(); 17 | const presaleEnd = theFuture.time(); 18 | const saleStart = theFuture.time(); 19 | const saleEnd = 2 ** 32 - 1; 20 | const royaltyBPS = 10_000; 21 | const feeManager = ethers.constants.AddressZero; 22 | const payoutAddress = ethers.constants.AddressZero; 23 | const contractURI = "http://localhost/contract/"; 24 | const metadataURI = "http://localhost/metadata/"; 25 | const metadataRendererInit = null; 26 | const tokenGateConfig = null; 27 | 28 | const scale = 1_000_000; 29 | const bigPercent = (num: BigNumber, perc: number) => num.div(100).mul(perc); 30 | 31 | describe("Splits", async () => { 32 | let owner: SignerWithAddress, 33 | addr1: SignerWithAddress, 34 | addr2: SignerWithAddress, 35 | addr3: SignerWithAddress, 36 | addr4: SignerWithAddress, 37 | sdk: Contract, 38 | nft: Contract, 39 | split: any[], 40 | splitMain: Contract; 41 | 42 | before(async () => { 43 | [addr1, addr2, addr3, addr4] = await ethers.getSigners(); 44 | sdk = await deployDCNTSDK(); 45 | splitMain = await deployContract('SplitMain'); 46 | nft = await deployDCNT4907A( 47 | sdk, 48 | name, 49 | symbol, 50 | hasAdjustableCap, 51 | isSoulbound, 52 | maxTokens, 53 | tokenPrice, 54 | maxTokenPurchase, 55 | presaleMerkleRoot, 56 | presaleStart, 57 | presaleEnd, 58 | saleStart, 59 | saleEnd, 60 | royaltyBPS, 61 | feeManager, 62 | payoutAddress, 63 | contractURI, 64 | metadataURI, 65 | metadataRendererInit, 66 | tokenGateConfig, 67 | ); 68 | 69 | const payouts = sortByAddress([ 70 | { 71 | address: addr2.address, 72 | percent: (scale / 100) * 90 73 | }, 74 | { 75 | address: addr3.address, 76 | percent: (scale / 100) * 10 77 | }, 78 | ]); 79 | 80 | const addresses = payouts.map(payout => payout.address); 81 | const percents = payouts.map(payout => payout.percent); 82 | const distributorFee = 0; 83 | split = [addresses, percents, distributorFee]; 84 | }); 85 | 86 | describe("createSplit()", async () => { 87 | it("should create a split", async () => { 88 | expect(await nft.splitWallet()).to.equal(ethers.constants.AddressZero); 89 | await nft.createSplit(splitMain.address, ...split); 90 | expect(await nft.splitWallet()).to.not.equal(ethers.constants.AddressZero); 91 | }); 92 | 93 | it("should revert if a split has already been created", async () => { 94 | await expect(nft.createSplit(splitMain.address, ...split)).to.be.revertedWith('Split already created'); 95 | }); 96 | 97 | it("should revert if called by an account which is not the owner", async () => { 98 | const freshNFT: Contract = await deployDCNT4907A( 99 | sdk, 100 | name, 101 | symbol, 102 | hasAdjustableCap, 103 | isSoulbound, 104 | maxTokens, 105 | tokenPrice, 106 | maxTokenPurchase, 107 | presaleMerkleRoot, 108 | presaleStart, 109 | presaleEnd, 110 | saleStart, 111 | saleEnd, 112 | royaltyBPS, 113 | feeManager, 114 | payoutAddress, 115 | contractURI, 116 | metadataURI, 117 | metadataRendererInit, 118 | tokenGateConfig, 119 | ); 120 | 121 | await expect( 122 | freshNFT.distributeERC20(ethers.constants.AddressZero, ...split, addr1.address) 123 | ).to.be.revertedWith('Split not created yet'); 124 | }); 125 | }); 126 | 127 | describe("distributeETH()", async () => { 128 | it("should transfer ETH to the split and distribute to receipients", async () => { 129 | await nft.mint(addr1.address, 1, { value: tokenPrice }); 130 | expect(await ethers.provider.getBalance(nft.address)).to.equal(tokenPrice); 131 | 132 | await nft.distributeETH(...split, addr1.address); 133 | expect(await ethers.provider.getBalance(nft.address)).to.equal(0); 134 | expect(await splitMain.getETHBalance(addr2.address)).to.equal(bigPercent(tokenPrice, 90)); 135 | expect(await splitMain.getETHBalance(addr3.address)).to.equal(bigPercent(tokenPrice, 10)); 136 | }); 137 | 138 | it("should revert if a split has not yet been created", async () => { 139 | const freshNFT: Contract = await deployDCNT4907A( 140 | sdk, 141 | name, 142 | symbol, 143 | hasAdjustableCap, 144 | isSoulbound, 145 | maxTokens, 146 | tokenPrice, 147 | maxTokenPurchase, 148 | presaleMerkleRoot, 149 | presaleStart, 150 | presaleEnd, 151 | saleStart, 152 | saleEnd, 153 | royaltyBPS, 154 | feeManager, 155 | payoutAddress, 156 | contractURI, 157 | metadataURI, 158 | metadataRendererInit, 159 | tokenGateConfig, 160 | ); 161 | 162 | await expect( 163 | freshNFT.distributeETH(...split, addr1.address) 164 | ).to.be.revertedWith('Split not created yet'); 165 | }); 166 | }); 167 | 168 | describe("distributeERC20()", async () => { 169 | it("should transfer ERC20 to the split and distribute to receipients", async () => { 170 | const supply = ethers.utils.parseEther('100'); 171 | const erc20 = await deployMockERC20(supply); 172 | await erc20.transfer(nft.address, supply); 173 | 174 | await nft.distributeERC20(erc20.address, ...split, addr1.address); 175 | 176 | const afterSplit = await erc20.balanceOf(splitMain.address); 177 | const after2 = await splitMain.getERC20Balance(addr2.address, erc20.address); 178 | const after3 = await splitMain.getERC20Balance(addr3.address, erc20.address); 179 | 180 | expect(afterSplit).to.equal(supply.sub(1)); 181 | expect(after2).to.equal(bigPercent(supply, 90).sub(1)); 182 | expect(after3).to.equal(bigPercent(supply, 10).sub(1)); 183 | }); 184 | 185 | it("should revert if a split has not yet been created", async () => { 186 | const freshNFT: Contract = await deployDCNT4907A( 187 | sdk, 188 | name, 189 | symbol, 190 | hasAdjustableCap, 191 | isSoulbound, 192 | maxTokens, 193 | tokenPrice, 194 | maxTokenPurchase, 195 | presaleMerkleRoot, 196 | presaleStart, 197 | presaleEnd, 198 | saleStart, 199 | saleEnd, 200 | royaltyBPS, 201 | feeManager, 202 | payoutAddress, 203 | contractURI, 204 | metadataURI, 205 | metadataRendererInit, 206 | tokenGateConfig, 207 | ); 208 | 209 | await expect( 210 | freshNFT.distributeERC20(ethers.constants.AddressZero, ...split, addr1.address) 211 | ).to.be.revertedWith('Split not created yet'); 212 | }); 213 | }); 214 | 215 | describe("distributeAndWithdraw()", async () => { 216 | before(async () => { 217 | sdk = await deployDCNTSDK(); 218 | splitMain = await deployContract('SplitMain'); 219 | nft = await deployDCNT4907A( 220 | sdk, 221 | name, 222 | symbol, 223 | hasAdjustableCap, 224 | isSoulbound, 225 | maxTokens, 226 | tokenPrice, 227 | maxTokenPurchase, 228 | presaleMerkleRoot, 229 | presaleStart, 230 | presaleEnd, 231 | saleStart, 232 | saleEnd, 233 | royaltyBPS, 234 | feeManager, 235 | payoutAddress, 236 | contractURI, 237 | metadataURI, 238 | metadataRendererInit, 239 | tokenGateConfig, 240 | ); 241 | }); 242 | 243 | it("should transfer ETH to the split, distribute to receipients, and withdraw", async () => { 244 | nft.createSplit(splitMain.address, ...split); 245 | 246 | await nft.mint(addr1.address, 1, { value: tokenPrice }); 247 | expect(await ethers.provider.getBalance(nft.address)).to.equal(tokenPrice); 248 | 249 | const before2 = await ethers.provider.getBalance(addr2.address); 250 | const before3 = await ethers.provider.getBalance(addr3.address); 251 | 252 | await nft.distributeAndWithdraw(addr2.address, 1, [], ...split, addr1.address); 253 | expect(await ethers.provider.getBalance(nft.address)).to.equal(0); 254 | 255 | const after2 = await ethers.provider.getBalance(addr2.address); 256 | let after3 = await ethers.provider.getBalance(addr3.address); 257 | 258 | expect(after2).to.equal(before2.add(bigPercent(tokenPrice, 90).sub(1))); 259 | expect(after3).to.equal(before3); 260 | 261 | await nft.distributeAndWithdraw(addr3.address, 1, [], ...split, addr1.address); 262 | after3 = await ethers.provider.getBalance(addr3.address); 263 | expect(after3).to.equal(before3.add(bigPercent(tokenPrice, 10).sub(1))); 264 | }); 265 | 266 | it("should transfer ERC20s to the split, distribute to receipients, and withdraw", async () => { 267 | const supply = ethers.utils.parseEther('100'); 268 | 269 | const erc20A = await deployMockERC20(supply); 270 | await erc20A.transfer(nft.address, supply); 271 | 272 | const erc20B = await deployMockERC20(supply); 273 | await erc20B.transfer(nft.address, supply); 274 | 275 | await nft.distributeAndWithdraw( 276 | addr2.address, 277 | 1, 278 | [ 279 | erc20A.address, 280 | erc20B.address, 281 | ], 282 | ...split, 283 | addr1.address 284 | ); 285 | 286 | let after2 = await erc20A.balanceOf(addr2.address); 287 | expect(after2).to.equal(bigPercent(supply, 90).sub(2)); 288 | 289 | after2 = await erc20B.balanceOf(addr2.address); 290 | expect(after2).to.equal(bigPercent(supply, 90).sub(2)); 291 | }); 292 | 293 | it("should revert if a split has not yet been created", async () => { 294 | const freshNFT: Contract = await deployDCNT4907A( 295 | sdk, 296 | name, 297 | symbol, 298 | hasAdjustableCap, 299 | isSoulbound, 300 | maxTokens, 301 | tokenPrice, 302 | maxTokenPurchase, 303 | presaleMerkleRoot, 304 | presaleStart, 305 | presaleEnd, 306 | saleStart, 307 | saleEnd, 308 | royaltyBPS, 309 | feeManager, 310 | payoutAddress, 311 | contractURI, 312 | metadataURI, 313 | metadataRendererInit, 314 | tokenGateConfig, 315 | ); 316 | 317 | await expect( 318 | freshNFT.distributeAndWithdraw(addr2.address, 1, [], ...split, addr1.address) 319 | ).to.be.revertedWith('Split not created yet'); 320 | }); 321 | }); 322 | 323 | describe("transferToSplit()", async () => { 324 | it("should transfer ETH and ERC20 to the split", async () => { 325 | await nft.mint(addr1.address, 1, { value: tokenPrice }); 326 | expect(await ethers.provider.getBalance(nft.address)).to.equal(tokenPrice); 327 | 328 | const supply = ethers.utils.parseEther('100'); 329 | const erc20 = await deployMockERC20(supply); 330 | await erc20.transfer(nft.address, supply); 331 | expect(await erc20.balanceOf(nft.address)).to.equal(supply); 332 | 333 | await nft.transferToSplit(1, [erc20.address]); 334 | expect(await erc20.balanceOf(nft.splitWallet())).to.equal(supply); 335 | expect(await ethers.provider.getBalance(nft.splitWallet())).to.equal(tokenPrice); 336 | }); 337 | 338 | it("should revert if a split has not yet been created", async () => { 339 | const freshNFT: Contract = await deployDCNT4907A( 340 | sdk, 341 | name, 342 | symbol, 343 | hasAdjustableCap, 344 | isSoulbound, 345 | maxTokens, 346 | tokenPrice, 347 | maxTokenPurchase, 348 | presaleMerkleRoot, 349 | presaleStart, 350 | presaleEnd, 351 | saleStart, 352 | saleEnd, 353 | royaltyBPS, 354 | feeManager, 355 | payoutAddress, 356 | contractURI, 357 | metadataURI, 358 | metadataRendererInit, 359 | tokenGateConfig 360 | ); 361 | 362 | await expect( 363 | freshNFT.transferToSplit(1, []) 364 | ).to.be.revertedWith('Split not created yet'); 365 | }); 366 | }); 367 | }); 368 | -------------------------------------------------------------------------------- /test/ZKEditions.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ethers } from "hardhat"; 3 | import { before, beforeEach } from "mocha"; 4 | import { BigNumber, Contract } from "ethers"; 5 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 6 | import { deployDCNTSDK, deployZKEdition, deployMockERC721, theFuture, sortByAddress, base64decode } from "../core"; 7 | 8 | const name = 'Decent'; 9 | const symbol = 'DCNT'; 10 | const hasAdjustableCap = true; 11 | const isSoulbound = false; 12 | const maxTokens = 1; 13 | const tokenPrice = ethers.utils.parseEther('0.01'); 14 | const maxTokenPurchase = 2; 15 | const presaleMerkleRoot = null; 16 | const presaleStart = theFuture.time(); 17 | const presaleEnd = theFuture.time(); 18 | let saleStart = theFuture.time(); 19 | const saleEnd = theFuture.time() + theFuture.oneYear; 20 | const royaltyBPS = 10_00; 21 | const feeManager = ethers.constants.AddressZero; 22 | const payoutAddress = ethers.constants.AddressZero; 23 | const metadataRendererInit = { 24 | description: "This is the Decent unit test NFT", 25 | imageURI: "http://localhost/image.jpg", 26 | animationURI: "http://localhost/song.mp3", 27 | }; 28 | const contractURI = "http://localhost/contract/"; 29 | const metadataURI = "http://localhost/metadata/"; 30 | const tokenGateConfig = { 31 | tokenAddress: ethers.constants.AddressZero, 32 | minBalance: 0, 33 | saleType: 0, 34 | } 35 | 36 | describe("ZKEdition", async () => { 37 | let owner: SignerWithAddress, 38 | addr1: SignerWithAddress, 39 | addr2: SignerWithAddress, 40 | addr3: SignerWithAddress, 41 | addr4: SignerWithAddress, 42 | sdk: Contract, 43 | clone: Contract, 44 | nft: Contract, 45 | metadataRenderer: Contract, 46 | split: any[], 47 | parentIP: Contract; 48 | 49 | describe("initialize()", async () => { 50 | before(async () => { 51 | [owner] = await ethers.getSigners(); 52 | sdk = await deployDCNTSDK(); 53 | parentIP = await deployMockERC721(); 54 | metadataRenderer = await ethers.getContractAt('DCNTMetadataRenderer', sdk.metadataRenderer()); 55 | await theFuture.reset(); 56 | clone = await deployZKEdition( 57 | sdk, 58 | name, 59 | symbol, 60 | hasAdjustableCap, 61 | isSoulbound, 62 | maxTokens, 63 | tokenPrice, 64 | maxTokenPurchase, 65 | presaleMerkleRoot, 66 | presaleStart, 67 | presaleEnd, 68 | saleStart, 69 | saleEnd, 70 | royaltyBPS, 71 | feeManager, 72 | payoutAddress, 73 | contractURI, 74 | metadataURI, 75 | metadataRendererInit, 76 | tokenGateConfig, 77 | ethers.constants.AddressZero, 78 | parentIP.address 79 | ); 80 | }); 81 | 82 | it("should have the owner set as the EOA deploying the contract", async () => { 83 | expect(ethers.utils.getAddress(await clone.owner())).to.equal(owner.address); 84 | }); 85 | 86 | it("should initialize state which would otherwise be set in constructor", async () => { 87 | expect(await clone.name()).to.equal(name); 88 | expect(await clone.symbol()).to.equal(symbol); 89 | expect(await clone.hasAdjustableCap()).to.equal(hasAdjustableCap); 90 | expect(await clone.MAX_TOKENS()).to.equal(maxTokens); 91 | }); 92 | 93 | it("should have the verifier set to zero addres", async () => { 94 | expect(ethers.utils.getAddress(await clone.zkVerifier())).to.equal(ethers.constants.AddressZero); 95 | }); 96 | }); 97 | 98 | describe("zkClaim()", async () => { 99 | before(async () => { 100 | [addr1, addr2, addr3, addr4] = await ethers.getSigners(); 101 | const sdk = await deployDCNTSDK(); 102 | nft = await deployZKEdition( 103 | sdk, 104 | name, 105 | symbol, 106 | hasAdjustableCap, 107 | isSoulbound, 108 | maxTokens, 109 | tokenPrice, 110 | maxTokenPurchase, 111 | presaleMerkleRoot, 112 | presaleStart, 113 | presaleEnd, 114 | saleStart, 115 | saleEnd, 116 | royaltyBPS, 117 | feeManager, 118 | payoutAddress, 119 | contractURI, 120 | metadataURI, 121 | metadataRendererInit, 122 | tokenGateConfig, 123 | addr1.address, 124 | parentIP.address 125 | ); 126 | }); 127 | 128 | it("should mint tokens to the specified recipient", async () => { 129 | const freshNFT = await deployZKEdition( 130 | sdk, 131 | name, 132 | symbol, 133 | hasAdjustableCap, 134 | isSoulbound, 135 | maxTokens, 136 | tokenPrice, 137 | maxTokenPurchase, 138 | presaleMerkleRoot, 139 | presaleStart, 140 | presaleEnd, 141 | saleStart, 142 | saleEnd, 143 | royaltyBPS, 144 | feeManager, 145 | payoutAddress, 146 | contractURI, 147 | metadataURI, 148 | metadataRendererInit, 149 | tokenGateConfig, 150 | addr1.address, 151 | parentIP.address 152 | ); 153 | 154 | expect(await freshNFT.balanceOf(addr1.address)).to.equal(0); 155 | 156 | await freshNFT.zkClaim(addr1.address); 157 | expect(await freshNFT.balanceOf(addr1.address)).to.equal(1); 158 | }); 159 | 160 | it("should prevent a mint which would exceed max supply", async () => { 161 | await nft.zkClaim(addr1.address) 162 | await expect(nft.zkClaim(addr1.address)).to.be.revertedWith( 163 | 'Purchase would exceed max supply' 164 | ); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------