├── .gas-snapshot ├── .github └── workflows │ ├── forge_tests.yml │ └── slither.yml ├── .gitignore ├── .gitmodules ├── .prettierrc ├── README.md ├── TODO.md ├── foundry.toml ├── lcov.info ├── package-lock.json ├── package.json ├── script ├── Deploy.s.sol ├── DeployAndConfigureProxy.s.sol ├── UpdateMetadataBaseURI.s.sol └── UpgradeMetadataContract.s.sol ├── scripts ├── boot.js ├── call.js ├── compile.js ├── deploy.js ├── index.js ├── qa.js └── serve.js ├── src ├── BoundLayerable.sol ├── Renderer.sol ├── SVG.sol ├── Utils.sol ├── examples │ └── BoundLayerableFirstComposedCutoff.sol ├── implementations │ ├── BoundLayerableFirstComposedCutoffImpl.sol │ ├── BoundLayerableSnapshotImpl.sol │ ├── BoundLayerableTestImpl.sol │ ├── TestToken.sol │ └── TestnetToken.sol ├── interface │ ├── Constants.sol │ ├── Enums.sol │ ├── Errors.sol │ ├── Events.sol │ └── Structs.sol ├── lib │ ├── BitMapUtility.sol │ ├── JSON.sol │ └── PackedByteUtility.sol ├── metadata │ ├── IImageLayerable.sol │ ├── ILayerable.sol │ ├── ImageLayerable.sol │ └── Layerable.sol ├── token │ └── ERC721A.sol ├── traits │ ├── OnChainMultiTraits.sol │ ├── OnChainTraits.sol │ ├── RandomTraits.sol │ └── RandomTraitsImpl.sol └── vrf │ └── BatchVRFConsumer.sol ├── test ├── BoundLayerable.t.sol ├── BoundLayerableFuzz.t.sol ├── BoundLayerableSnapshot.t.sol ├── Token.t.sol ├── TokenBulkBurn.t.sol ├── examples │ ├── BoundLayerableFirstComposedCutoff.t.sol │ └── BoundLayerableFirstComposedCutoffSnapshot.t.sol ├── helpers │ ├── StringTestUtility.sol │ └── StringTestUtility.t.sol ├── lib │ ├── BitMapUtility.t.sol │ ├── JSON.t.sol │ └── PackedByteUtility.t.sol ├── metadata │ ├── ImageLayerable.t.sol │ └── Layerable.t.sol ├── traits │ ├── OnChainMultiTraits.t.sol │ ├── OnChainTraits.t.sol │ └── RandomTraits.t.sol ├── util │ └── ERC721Recipient.sol └── vrf │ └── BatchVRFConsumer.t.sol └── yarn.lock /.gas-snapshot: -------------------------------------------------------------------------------- 1 | BoundLayerableTest:testBurnAndBindMultiple() (gas: 172324) 2 | BoundLayerableTest:testBurnAndBindMultipleAndSetActiveLayers() (gas: 183000) 3 | BoundLayerableTest:testBurnAndBindMultipleBatchNotRevealed() (gas: 15308) 4 | BoundLayerableTest:testBurnAndBindMultiple_CannotBindBase() (gas: 24302) 5 | BoundLayerableTest:testBurnAndBindMultiple_LayerAlreadyBound() (gas: 38733) 6 | BoundLayerableTest:testBurnAndBindMultiple_NotOwner() (gas: 116297) 7 | BoundLayerableTest:testBurnAndBindMultiple_OnlyBase() (gas: 15823) 8 | BoundLayerableTest:testBurnAndBindSingle() (gas: 117973) 9 | BoundLayerableTest:testBurnAndBindSingleAndSetActiveLayers() (gas: 128500) 10 | BoundLayerableTest:testBurnAndBindSingleBatchNotRevealed() (gas: 28213) 11 | BoundLayerableTest:testBurnAndBindSingle_CannotBindBase() (gas: 21454) 12 | BoundLayerableTest:testBurnAndBindSingle_LayerAlreadyBound() (gas: 37917) 13 | BoundLayerableTest:testBurnAndBindSingle_NotOwner() (gas: 106816) 14 | BoundLayerableTest:testBurnAndBindSingle_OnlyBase() (gas: 22028) 15 | BoundLayerableTest:testCheckUnpackedIsSubsetOfBound1() (gas: 12373) 16 | BoundLayerableTest:testGetActiveLayers() (gas: 57582) 17 | BoundLayerableTest:testGetActiveLayers(uint8) (runs: 4096, μ: 43326, ~: 43802) 18 | BoundLayerableTest:testGetActiveLayersNoLayers() (gas: 8534) 19 | BoundLayerableTest:testGetTokenURI() (gas: 158536) 20 | BoundLayerableTest:testGetTokenURIMocked() (gas: 46195) 21 | BoundLayerableTest:testSetActiveLayers() (gas: 30775) 22 | BoundLayerableTest:testSetActiveLayers_NotOwner() (gas: 106848) 23 | BoundLayerableTest:testSetActiveLayers_OnlyBase() (gas: 38713) 24 | BoundLayerableTest:testSetMetadataContract() (gas: 2935409) 25 | BoundLayerableTest:testSetMetadataContract_onlyOwner(address) (runs: 4096, μ: 2933145, ~: 2933145) 26 | BoundLayerableTest:testUnpackLayersToBitMapAndCheckForDuplicates() (gas: 72540) 27 | BoundLayerableTest:testgetBoundLayers(uint8) (runs: 4096, μ: 197839, ~: 109491) 28 | BoundLayerableFuzzTest:testFuzzCheckUnpackedIsSubsetOfBound(uint256,uint256) (runs: 4096, μ: 9858, ~: 9841) 29 | BoundLayerableSnapshotTest:test_snapshotBurnAndBindMultiple1() (gas: 266262) 30 | BoundLayerableSnapshotTest:test_snapshotBurnAndBindMultipleAndSetActive() (gas: 278060) 31 | BoundLayerableSnapshotTest:test_snapshotBurnAndBindMultipleTransferred() (gas: 150233) 32 | BoundLayerableSnapshotTest:test_snapshotBurnAndBindSingle() (gas: 109182) 33 | BoundLayerableSnapshotTest:test_snapshotBurnAndBindSingleTransferred() (gas: 73405) 34 | BoundLayerableSnapshotTest:test_snapshotMintFive() (gas: 105625) 35 | BoundLayerableSnapshotTest:test_snapshotMintSingle() (gas: 95976) 36 | BoundLayerableSnapshotTest:test_snapshotSetActiveLayers() (gas: 19838) 37 | TestTokenTest:testDoTheMost() (gas: 475136) 38 | TokenBulkBurnTest:test_snapshotBulkBindLayers() (gas: 123987495) 39 | TokenBulkBurnTest:test_snapshotDisableTradingAndBurn() (gas: 150464877) 40 | BoundLayerableFirstComposedCutoffTest:testBurnAndBindMultiple() (gas: 172940) 41 | BoundLayerableFirstComposedCutoffTest:testBurnAndBindMultipleBatchNotRevealed() (gas: 15241) 42 | BoundLayerableFirstComposedCutoffTest:testBurnAndBindMultiple_afterCutoff() (gas: 172751) 43 | BoundLayerableFirstComposedCutoffTest:testBurnAndBindSingle() (gas: 118522) 44 | BoundLayerableFirstComposedCutoffTest:testBurnAndBindSingleBatchNotRevealed() (gas: 28178) 45 | BoundLayerableFirstComposedCutoffTest:testBurnAndBindSingle_afterCutoff() (gas: 118229) 46 | BoundLayerableFirstComposedCutoffTest:testBurnAndBindSingle_beforeThenAfterCutoff() (gas: 157692) 47 | BoundLayerableFirstComposedCutoffTest:testCheckUnpackedIsSubsetOfBound() (gas: 12305) 48 | BoundLayerableFirstComposedCutoffTest:testGetActiveLayers() (gas: 74627) 49 | BoundLayerableFirstComposedCutoffTest:testGetActiveLayers(uint8) (runs: 4096, μ: 58153, ~: 60825) 50 | BoundLayerableFirstComposedCutoffTest:testGetActiveLayersNoLayers() (gas: 8390) 51 | BoundLayerableFirstComposedCutoffTest:testSetActiveLayers() (gas: 47853) 52 | BoundLayerableFirstComposedCutoffTest:testSetMetadataContract() (gas: 2935409) 53 | BoundLayerableFirstComposedCutoffTest:testSetMetadataContract_onlyOwner(address) (runs: 4096, μ: 2933101, ~: 2933101) 54 | BoundLayerableFirstComposedCutoffTest:testUnpackLayersToBitMapAndCheckForDuplicates() (gas: 41868) 55 | BoundLayerableFirstComposedCutoffTest:testgetBoundLayers(uint8) (runs: 4096, μ: 199363, ~: 109359) 56 | BoundLayerableFirstComposedCutoffSnapshotTest:test_snapshotBurnAndBindMultiple1() (gas: 267424) 57 | BoundLayerableFirstComposedCutoffSnapshotTest:test_snapshotBurnAndBindMultipleTransferred() (gas: 151395) 58 | BoundLayerableFirstComposedCutoffSnapshotTest:test_snapshotBurnAndBindSingle() (gas: 109559) 59 | BoundLayerableFirstComposedCutoffSnapshotTest:test_snapshotBurnAndBindSingleTransferred() (gas: 73782) 60 | BoundLayerableFirstComposedCutoffSnapshotTest:test_snapshotSetActiveLayers() (gas: 36939) 61 | StringtestUtilityTest:testContainsString() (gas: 64430) 62 | StringtestUtilityTest:testEndsWith() (gas: 7744) 63 | StringtestUtilityTest:testEndsWith(string) (runs: 4096, μ: 23755, ~: 19082) 64 | StringtestUtilityTest:testEquals(string) (runs: 4096, μ: 3155, ~: 3218) 65 | StringtestUtilityTest:testFuzzContains(string) (runs: 4096, μ: 128485, ~: 107830) 66 | StringtestUtilityTest:testStartsWith() (gas: 4048) 67 | StringtestUtilityTest:testStartsWith(string) (runs: 4096, μ: 11520, ~: 9392) 68 | BitMapUtilityTest:testContains(uint8) (runs: 4096, μ: 411, ~: 411) 69 | BitMapUtilityTest:testIsSupersetOf(uint256,uint256) (runs: 4096, μ: 373, ~: 373) 70 | BitMapUtilityTest:testIsSupersetOfNotSuperset(uint256,uint256) (runs: 4096, μ: 535, ~: 517) 71 | BitMapUtilityTest:testLsb(uint8,uint256) (runs: 4096, μ: 3580, ~: 3580) 72 | BitMapUtilityTest:testLsbZero() (gas: 226) 73 | BitMapUtilityTest:testMsb(uint8,uint256) (runs: 4096, μ: 892, ~: 901) 74 | BitMapUtilityTest:testMsbZero() (gas: 258) 75 | BitMapUtilityTest:testToBitMap(uint8) (runs: 4096, μ: 429, ~: 429) 76 | BitMapUtilityTest:testUintsToBitMap(uint8[256]) (runs: 4096, μ: 170437, ~: 170437) 77 | BitMapUtilityTest:testUnpackBitMap(uint8) (runs: 4096, μ: 81676, ~: 47016) 78 | BitMapUtilityTest:testUnpackBitMap1and255() (gas: 1179) 79 | BitMapUtilityTest:testUnpackBitMap32Ones() (gas: 17635) 80 | BitMapUtilityTest:testUnpackBitMapOopsAllOnes() (gas: 137737) 81 | BitMapUtilityTest:test_fuzzLsb(uint256) (runs: 4096, μ: 301, ~: 301) 82 | BitMapUtilityTest:test_fuzzMsb(uint256) (runs: 4096, μ: 464, ~: 464) 83 | JsonTest:testArray(string) (runs: 4096, μ: 2339, ~: 2356) 84 | JsonTest:testArrayOf(string,uint8) (runs: 4096, μ: 3050341, ~: 530422) 85 | JsonTest:testArrayOfTwo(string,uint8,uint8) (runs: 4096, μ: 1707207, ~: 1213459) 86 | JsonTest:testJoinComma() (gas: 1751) 87 | JsonTest:testJoinComma(string,uint8) (runs: 4096, μ: 2530491, ~: 439435) 88 | JsonTest:testObject(string) (runs: 4096, μ: 2337, ~: 2354) 89 | JsonTest:testObjectOf(string,string,uint8) (runs: 4096, μ: 8477125, ~: 1075873) 90 | JsonTest:testProperty(string,string) (runs: 4096, μ: 55515, ~: 45952) 91 | JsonTest:testQuote(string) (runs: 4096, μ: 41412, ~: 34127) 92 | JsonTest:testRawProperty(string,string) (runs: 4096, μ: 54148, ~: 44581) 93 | PackedByteUtilityTest:testGetPackedByteFromLeft(uint8,uint8) (runs: 4096, μ: 723, ~: 723) 94 | PackedByteUtilityTest:testGetPackedByteFromRight(uint8,uint8) (runs: 4096, μ: 859, ~: 859) 95 | PackedByteUtilityTest:testGetPackedBytesFromLeft() (gas: 801) 96 | PackedByteUtilityTest:testGetPackedBytesFromRight() (gas: 780) 97 | PackedByteUtilityTest:testGetPackedNFromRight() (gas: 425) 98 | PackedByteUtilityTest:testGetPackedShortFromLeft(uint16,uint8) (runs: 4096, μ: 2574, ~: 2574) 99 | PackedByteUtilityTest:testGetPackedShortFromRight(uint16,uint8) (runs: 4096, μ: 2756, ~: 2756) 100 | PackedByteUtilityTest:testPackArrayOfBytes() (gas: 9537) 101 | PackedByteUtilityTest:testPackArrayOfBytes(uint8[32]) (runs: 4096, μ: 26997, ~: 26997) 102 | PackedByteUtilityTest:testPackArrayOfShorts() (gas: 38751) 103 | PackedByteUtilityTest:testPackArrayOfShorts(uint16[32]) (runs: 4096, μ: 51661, ~: 51661) 104 | PackedByteUtilityTest:testPackArraysOfBytes() (gas: 37105) 105 | PackedByteUtilityTest:testPackByteAtIndex(uint8,uint8) (runs: 4096, μ: 3639, ~: 3890) 106 | PackedByteUtilityTest:testPackNAtRightIndex(uint256,uint8,uint256) (runs: 4096, μ: 15605, ~: 15605) 107 | PackedByteUtilityTest:testUnpackBytes() (gas: 2169) 108 | PackedByteUtilityTest:testUnpackBytesToBitmap(uint8[32]) (runs: 4096, μ: 38326, ~: 38326) 109 | ImageLayerableTest:testGetLayerJson(uint256) (runs: 4096, μ: 114208, ~: 106446) 110 | ImageLayerableTest:testGetTokenJson(uint256) (runs: 4096, μ: 226778, ~: 210436) 111 | ImageLayerableTest:testGetTokenJson1() (gas: 517699) 112 | ImageLayerableTest:testGetTokenURI() (gas: 763689) 113 | ImageLayerableTest:testInitialize_InvalidInitialization() (gas: 10417) 114 | ImageLayerableTest:testInitialize_noCode() (gas: 219738) 115 | ImageLayerableTest:testSetBaseLayerURI() (gas: 16508) 116 | ImageLayerableTest:testSetDefaultURI() (gas: 16594) 117 | ImageLayerableTest:testSetDefaultURI_onlyOwner() (gas: 11282) 118 | ImageLayerableTest:testSetDescription() (gas: 16596) 119 | ImageLayerableTest:testSetExternalUrl() (gas: 16573) 120 | ImageLayerableTest:testSetHeight() (gas: 13550) 121 | ImageLayerableTest:testSetHeight_onlyOwner() (gas: 10863) 122 | ImageLayerableTest:testSetWidth() (gas: 13537) 123 | ImageLayerableTest:testSetWidth_onlyOwner() (gas: 10820) 124 | LayerableTest:testBoundLayerTraits(uint8[2]) (runs: 4096, μ: 1416217, ~: 1428089) 125 | LayerableTest:testGetActiveLayerTraits(uint8[2]) (runs: 4096, μ: 988348, ~: 1043413) 126 | LayerableTest:testInitialize() (gas: 3104026) 127 | LayerableTest:testInitialize_InvalidInitialization() (gas: 8961) 128 | OnChainMultiTraitsTest:testGetLayerTraitJson() (gas: 296616) 129 | OnChainMultiTraitsTest:testSetAttribute_onlyOwner(address) (runs: 4096, μ: 85787, ~: 85787) 130 | OnChainMultiTraitsTest:testSetAttributes() (gas: 187716) 131 | OnChainMultiTraitsTest:testSetAttributes_onlyOwner(address) (runs: 4096, μ: 161749, ~: 161749) 132 | OnChainTraitsTest:testGetAttributeJson() (gas: 42727) 133 | OnChainTraitsTest:testGetLayerTraitJson() (gas: 396390) 134 | OnChainTraitsTest:testSetAttribute_onlyOwner(address) (runs: 4096, μ: 61934, ~: 61934) 135 | OnChainTraitsTest:testSetAttributes() (gas: 139064) 136 | OnChainTraitsTest:testSetAttributes_mismatch() (gas: 13539) 137 | OnChainTraitsTest:testSetAttributes_onlyOwner(address) (runs: 4096, μ: 114647, ~: 114647) 138 | RandomTraitsTest:testGetLayerId(uint8,uint8,uint8) (runs: 4096, μ: 54207, ~: 44565) 139 | RandomTraitsTest:testGetLayerIdBounds(uint256) (runs: 4096, μ: 106285, ~: 106286) 140 | RandomTraitsTest:testGetLayerIdBounds(uint256,uint8) (runs: 4096, μ: 425127, ~: 366336) 141 | RandomTraitsTest:testGetLayerIdBoundsDirect(uint256,uint8,uint8,uint16) (runs: 4096, μ: 404899, ~: 326351) 142 | RandomTraitsTest:testGetLayerId_NoDistributions() (gas: 14198) 143 | RandomTraitsTest:testGetLayerId_badDistribution_layerType6_index31() (gas: 55587) 144 | RandomTraitsTest:testGetLayerId_badDistribution_layerType6_index32() (gas: 55740) 145 | RandomTraitsTest:testGetLayerId_badDistribution_layerType7_index31() (gas: 58228) 146 | RandomTraitsTest:testGetLayerId_badDistribution_layerType7_index32() (gas: 58415) 147 | RandomTraitsTest:testGetLayerSeedShifts() (gas: 11852) 148 | RandomTraitsTest:testGetLayerType() (gas: 21380) 149 | RandomTraitsTest:testSetLayerTypeDistribution(uint8,uint256[2]) (runs: 4096, μ: 56721, ~: 58364) 150 | RandomTraitsTest:testSetLayerTypeDistributionInvalidLayerType(uint8) (runs: 4096, μ: 13846, ~: 13846) 151 | RandomTraitsTest:testSetLayerTypeDistributionNotOwner(address) (runs: 4096, μ: 11769, ~: 11769) 152 | RandomTraitsTest:testSetLayerTypeDistributions() (gas: 249047) 153 | BatchVRFConsumerTest:testCheckAndReturnNumBatches(uint8,uint8,bool) (runs: 4096, μ: 63831, ~: 63118) 154 | BatchVRFConsumerTest:testClearPendingReveal() (gas: 66886) 155 | BatchVRFConsumerTest:testClearPendingReveal_onlyOwner(address) (runs: 4096, μ: 11242, ~: 11242) 156 | BatchVRFConsumerTest:testConstructorEnforcesPowerOfTwo() (gas: 38853717) 157 | BatchVRFConsumerTest:testFulfillRandomWords(uint8,uint8) (runs: 4096, μ: 76350, ~: 64033) 158 | BatchVRFConsumerTest:testFulfillRandomWords123() (gas: 97423) 159 | BatchVRFConsumerTest:testFulfillRandomnessClearsPendingReveal() (gas: 107017) 160 | BatchVRFConsumerTest:testFulfillRandomnessDoesNotOverWriteExistingSeed() (gas: 86228) 161 | BatchVRFConsumerTest:testGetRandomnessForTokenId(uint256) (runs: 4096, μ: 61623, ~: 61623) 162 | BatchVRFConsumerTest:testGetRandomnessForTokenId_irl() (gas: 28622) 163 | BatchVRFConsumerTest:testGetRandomnessForTokenId_notRevealed(uint256) (runs: 4096, μ: 7911, ~: 7911) 164 | BatchVRFConsumerTest:testNonZeroRandomness() (gas: 107271) 165 | BatchVRFConsumerTest:testRawFulfillRandomWords123() (gas: 113033) 166 | BatchVRFConsumerTest:testRawFulfillRandomWords_onlyCoordinator(address) (runs: 4096, μ: 10806, ~: 10806) 167 | BatchVRFConsumerTest:testRequestMaxRandomness() (gas: 55861) 168 | BatchVRFConsumerTest:testRequestNoFullBatchMinted() (gas: 13609) 169 | BatchVRFConsumerTest:testRequestNoFullBatchMinted_ForceUnsafe() (gas: 81421) 170 | BatchVRFConsumerTest:testRequestRandomWords() (gas: 59169) 171 | BatchVRFConsumerTest:testRequestRandomWords(uint8,uint8) (runs: 4096, μ: 66641, ~: 66808) 172 | BatchVRFConsumerTest:testRequestRandomWordsAllNoneBatched() (gas: 57304) 173 | BatchVRFConsumerTest:testRequestRandomWordsAllSomeBatched() (gas: 56220) 174 | BatchVRFConsumerTest:testRequestRandomWordsSomeNoneBatched() (gas: 59259) 175 | BatchVRFConsumerTest:testRequestRandomWordsSomeSomeBatched() (gas: 60279) 176 | BatchVRFConsumerTest:testRequestRandomness_NoBatchesToReveal() (gas: 104052) 177 | BatchVRFConsumerTest:testRequestRandomness_PendingReveal() (gas: 60563) 178 | BatchVRFConsumerTest:testSetForceUnsafeReveal() (gas: 34102) 179 | BatchVRFConsumerTest:test_snapshotGetRandomnessForTokenIdFromSeed1() (gas: 5612) 180 | -------------------------------------------------------------------------------- /.github/workflows/forge_tests.yml: -------------------------------------------------------------------------------- 1 | name: Forge Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | forge-tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - name: Install Foundry 12 | uses: onbjerg/foundry-toolchain@v1 13 | with: 14 | version: nightly 15 | 16 | - name: Install dependencies 17 | run: forge install 18 | 19 | - name: Run forge tests 20 | run: forge test -vvv 21 | 22 | - name: Run snapshot 23 | run: forge snapshot -------------------------------------------------------------------------------- /.github/workflows/slither.yml: -------------------------------------------------------------------------------- 1 | name: Slither Analysis 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | analyze: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: crytic/slither-action@v0.1.1 13 | with: 14 | target: 'src/' 15 | slither-args: '--exclude-informational --checklist --exclude variable-scope' 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | cache/ 3 | .* 4 | node_modules/ 5 | broadcast/ 6 | img 7 | *.txt 8 | html/ 9 | lcov.info -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/solmate"] 5 | path = lib/solmate 6 | url = https://github.com/rari-capital/solmate 7 | [submodule "lib/openzeppelin-contracts"] 8 | path = lib/openzeppelin-contracts 9 | url = https://github.com/openzeppelin/openzeppelin-contracts 10 | [submodule "lib/ERC721A"] 11 | path = lib/ERC721A 12 | url = https://github.com/chiru-labs/ERC721A 13 | [submodule "lib/chainlink"] 14 | path = lib/chainlink 15 | url = https://github.com/smartcontractkit/chainlink 16 | [submodule "lib/utility-contracts"] 17 | path = lib/utility-contracts 18 | url = https://github.com/jameswenzel/utility-contracts 19 | [submodule "lib/solady"] 20 | path = lib/solady 21 | url = https://github.com/vectorized/solady 22 | [submodule "lib/solenv"] 23 | path = lib/solenv 24 | url = https://github.com/memester-xyz/solenv 25 | [submodule "lib/hot-chain-svg"] 26 | path = lib/hot-chain-svg 27 | url = https://github.com/jameswenzel/hot-chain-svg 28 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "printWidth": 120, 4 | "useTabs": false, 5 | "bracketSpacing": true 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BoundLayerable 2 | 3 | BoundLayerable is a set of smart contracts for minting and then composing layerable NFTs on-chain. 4 | 5 | ## The BoundLayerable flow: 6 | 7 | - A user mints a set of N "layers" efficiently using ERC721A 8 | - The first is a "base" or "bindable" layer 9 | - Layers are revealed on-chain 10 | - Users can burn a layer to "bind" it to their base layer 11 | - Users can update the base layer's metadata on-chain to show/hide and reorder layers 12 | 13 | ## Technical specs 14 | 15 | - Secure on-chain randomness using ChainLink VRF 16 | - 8 types of layers 17 | - Up to 32 unique layers per "type" elements, except the 8th type, which supports 31 unique layers 18 | - 255 total unique layers 19 | - 16-bit granularity (~0.0015%) for trait rarity 20 | - Up to 32 "Active" layers at once 21 | - Traits and metadata stored on-chain 22 | - Updateable metadata contract 23 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | EXPAND: 2 | 3 | - configurable lock 4 | - lock layers from transfer? 5 | - use setAux to track date composed 6 | - limited signature layer for those minted in first week 7 | - first x slime holders 8 | - authorized burn function for future composability? 9 | - [ ] unset forceUnsafeReveal when unsafe revealing 10 | 11 | Features: 12 | 13 | - [ ] add base name to metadata contract 14 | - [x] add "Layer Count" to base layers 15 | - [x] on-chain VRF for reveals 16 | - [x] batched reveals 17 | - [x] implement 18 | - [x] separate metadata into separate contract 19 | - [x] investigate manually binding tokens owned by binder 20 | - [x] TwoStepOwnable 21 | - [x] implement 22 | - [x] Commission/Withdrawable 23 | - [x] implement 24 | - [x] MaxMintable etc 25 | - [x] implement 26 | - [x] allowlist 27 | - [x] implement 28 | - [x] BoundLayerable 29 | - [x] Layerable 30 | - [ ] Token.sol should be full-fledged token with all utils 31 | - [x] decide on Variations 32 | - [ ] punted for now, need to consider how to optimize 33 | - [ ] Placeholder image per layerType? 34 | - [ ] admin role in addition to owner? 35 | - [ ] EIP-2981 36 | 37 | Optimizations: 38 | 39 | - [ ] burnAndBindSingleAndSetActiveLayers methods? 40 | - [x] use uint256s everywhere instead of uint8s 41 | - [x] Genericize LayerType 42 | - [x] genericize getLayerType 43 | - [ ] remove DisplayType from Attribute? 44 | - [ ] consider storing Attributes using SSTORE2 45 | - [ ] probably punt to later version 46 | - [ ] consider removing vrfCoordinatorAddress as constructor param and set via chainId (larger deploy size) 47 | 48 | Cleanup: 49 | 50 | - [ ] natspec comments 51 | - [x] BoundLayerable 52 | - [x] PackedByteUtility 53 | - [x] BitMapUtility 54 | - [x] Layerable 55 | - [x] ImageLayerable 56 | - [x] JSON 57 | - [x] remove leading underscores where not necessary to disambiguate 58 | - [x] Split main Layerable functionality out and make ImageLayerable an example contract 59 | - [x] rename bitField to bitMap 60 | - [ ] more helper contracts? 61 | - [ ] rename BatchVRFConsumer 62 | - [ ] remove/update todos in comments 63 | - [x] rename traitGenerationSeed 64 | - [x] remove maxmintable etc and import utility-contracts 65 | - [ ] figure out why forge doesn't replace revert codes w error name 66 | - [ ] make subscription mutable? 67 | 68 | Tests: 69 | 70 | - [ ] test that switch to uint256s over uint8s doesn't allow anything weird 71 | - [ ] test that switch to uint32 disguised as bytes32 for traitgenerationseed doesn't allow anything weird 72 | - [x] PackedByteUtility 73 | - [x] BitMapUtility 74 | - [x] BoundLayerable 75 | - [x] RandomTraits 76 | - [ ] modifiers 77 | - [ ] Layerable 78 | - [ ] ImageLayerable 79 | - [ ] BoundLayerable -> Layerable 80 | - [ ] BoundLayerable new combined BurnAndSetActive methods 81 | 82 | Integration/e2e tests: 83 | 84 | - [ ] e2e tests for chainlink vrf 85 | - [ ] add "integration" foundry profile 86 | - [ ] add suite of integration tests that run against a testnet when "integration" foundry profile is active 87 | 88 | v0.2 89 | 90 | - [ ] variations 91 | - [ ] sstore2 92 | - [ ] multiple attributes per layer 93 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | fuzz_runs = 2048 6 | solc_version = '0.8.15' 7 | optimizer_runs = 1_000_000 8 | optimizer = true 9 | remappings = [ 10 | 'ERC721A/=lib/ERC721A/contracts/', 11 | 'chainlink/=lib/chainlink/', 12 | 'ds-test/=lib/solmate/lib/ds-test/src/', 13 | 'forge-std/=lib/forge-std/src/', 14 | 'hot-chain-svg/=lib/hot-chain-svg/contracts/', 15 | 'openzeppelin-contracts/=lib/openzeppelin-contracts/', 16 | 'solmate/=lib/solmate/src/', 17 | 'utility-contracts/=lib/utility-contracts/src/', 18 | 'bound-layerable=src/' 19 | ] 20 | 21 | 22 | [fuzz] 23 | runs = 4096 24 | 25 | [profile.ir] 26 | via_ir = true 27 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hot-chain-svg", 3 | "version": "0.0.1", 4 | "main": "src/index.js", 5 | "author": "w1nt3r.eth", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "node scripts/index.js", 9 | "qa": "node scripts/qa.js" 10 | }, 11 | "prettier": { 12 | "singleQuote": true 13 | }, 14 | "dependencies": { 15 | "@ethereumjs/vm": "^5.8.0", 16 | "@ethersproject/abi": "^5.6.0", 17 | "@openzeppelin/contracts": "^4.5.0", 18 | "@rari-capital/solmate": "^6.2.0", 19 | "solc": "0.8.13", 20 | "xmldom": "^0.6.0" 21 | }, 22 | "devDependencies": { 23 | "prettier": "^2.6.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /script/Deploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {Script} from 'forge-std/Script.sol'; 6 | import {TestnetToken} from '../src/implementations/TestnetToken.sol'; 7 | import {Layerable} from '../src/metadata/Layerable.sol'; 8 | import {ImageLayerable} from '../src/metadata/ImageLayerable.sol'; 9 | import {Attribute} from '../src/interface/Structs.sol'; 10 | import {DisplayType} from '../src/interface/Enums.sol'; 11 | import {Strings} from 'openzeppelin-contracts/contracts/utils/Strings.sol'; 12 | import {Solenv} from 'solenv/Solenv.sol'; 13 | 14 | contract Deploy is Script { 15 | using Strings for uint256; 16 | 17 | struct AttributeTuple { 18 | uint256 traitId; 19 | string name; 20 | } 21 | 22 | function getLayerTypeStr(uint256 layerId) 23 | public 24 | pure 25 | returns (string memory result) 26 | { 27 | uint256 layerType = (layerId - 1) / 32; 28 | if (layerType == 0) { 29 | result = 'Portrait'; 30 | } else if (layerType == 1) { 31 | result = 'Background'; 32 | } else if (layerType == 2) { 33 | result = 'Border'; 34 | } else if (layerType == 5) { 35 | result = 'Texture'; 36 | } else if (layerType == 3 || layerType == 4) { 37 | result = 'Element'; 38 | } else { 39 | result = 'Special'; 40 | } 41 | } 42 | 43 | function setUp() public virtual { 44 | Solenv.config(); 45 | } 46 | 47 | function run() public { 48 | address deployer = vm.envAddress('DEPLOYER'); 49 | vm.startBroadcast(deployer); 50 | // if (chainId == 4) {coordinator 0x6168499c0cFfCaCD319c818142124B7A15E857ab 51 | // } else if (chainId == 137) { 52 | // coordinator = 0xAE975071Be8F8eE67addBC1A82488F1C24858067; 53 | // } else if (chainId == 80001) { 54 | // coordinator = 0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed; 55 | // } else { 56 | // coordinator = 0x271682DEB8C4E0901D1a1550aD2e64D568E69909; 57 | // } 58 | // emit log_named_address('coordinator', coordinator); 59 | 60 | AttributeTuple[164] memory attributeTuples = [ 61 | AttributeTuple(3, 'Portrait A3'), 62 | AttributeTuple(9, 'Portrait C1'), 63 | AttributeTuple(1, 'Portrait A4'), 64 | AttributeTuple(4, 'Portrait B2'), 65 | AttributeTuple(8, 'Portrait C2'), 66 | AttributeTuple(5, 'Portrait A2'), 67 | AttributeTuple(6, 'Portrait A1'), 68 | AttributeTuple(2, 'Portrait B3'), 69 | AttributeTuple(7, 'Portrait B1'), 70 | AttributeTuple(41, 'Cranium'), 71 | AttributeTuple(60, 'Dirty Grid Paper'), 72 | AttributeTuple(42, 'Disassembled'), 73 | AttributeTuple(44, 'Postal Worker'), 74 | AttributeTuple(56, 'Angled Gradient'), 75 | AttributeTuple(36, 'Haze'), 76 | AttributeTuple(35, 'Upside Down'), 77 | AttributeTuple(50, 'Shoebox'), 78 | AttributeTuple(62, 'Blue'), 79 | AttributeTuple(40, '100 Dollars'), 80 | AttributeTuple(45, 'Close-up'), 81 | AttributeTuple(37, 'Sticky Fingers'), 82 | AttributeTuple(38, 'Top Secret'), 83 | AttributeTuple(64, 'Off White'), 84 | AttributeTuple(34, 'Censorship Can!'), 85 | AttributeTuple(49, '13 Years Old'), 86 | AttributeTuple(53, 'Washed Out'), 87 | AttributeTuple(61, 'Grunge Paper'), 88 | AttributeTuple(54, 'Marbled Paper'), 89 | AttributeTuple(46, 'Gene Sequencing'), 90 | AttributeTuple(51, 'Geological Study'), 91 | AttributeTuple(48, 'Refractory Factory'), 92 | AttributeTuple(43, 'Day Trader'), 93 | AttributeTuple(58, 'Linear Gradient'), 94 | AttributeTuple(63, 'Red'), 95 | AttributeTuple(47, 'Seedphrase'), 96 | AttributeTuple(33, 'Split'), 97 | AttributeTuple(52, 'Clouds'), 98 | AttributeTuple(55, 'Warped Gradient'), 99 | AttributeTuple(39, 'Fractals'), 100 | AttributeTuple(59, 'Spheres'), 101 | AttributeTuple(57, 'Radial Gradient'), 102 | AttributeTuple(192, 'Subtle Dust'), 103 | AttributeTuple(167, 'Rips Bottom'), 104 | AttributeTuple(171, 'Restricted'), 105 | AttributeTuple(186, 'Dirty'), 106 | AttributeTuple(168, 'Crusty Journal'), 107 | AttributeTuple(181, 'Plastic & Sticker'), 108 | AttributeTuple(174, 'Folded Paper Stack'), 109 | AttributeTuple(177, 'Extreme Dust & Grime'), 110 | AttributeTuple(179, 'Folded Paper'), 111 | AttributeTuple(165, 'Rips Top'), 112 | AttributeTuple(180, 'Midline Destroyed'), 113 | AttributeTuple(184, 'Wax Paper'), 114 | AttributeTuple(182, 'Wrinkled'), 115 | AttributeTuple(163, 'Crinkled & Torn'), 116 | AttributeTuple(169, 'Burn It'), 117 | AttributeTuple(185, 'Wheatpasted'), 118 | AttributeTuple(162, 'Perfect Tear'), 119 | AttributeTuple(161, 'Puzzle'), 120 | AttributeTuple(176, 'Old Document'), 121 | AttributeTuple(172, 'Destroyed Edges'), 122 | AttributeTuple(187, 'Magazine Glare'), 123 | AttributeTuple(178, 'Water Damage'), 124 | AttributeTuple(189, 'Inked'), 125 | AttributeTuple(166, 'Rips Mid'), 126 | AttributeTuple(173, 'Grainy Cover'), 127 | AttributeTuple(175, 'Single Fold'), 128 | AttributeTuple(188, 'Scanner'), 129 | AttributeTuple(190, 'Heavy Dust & Scratches'), 130 | AttributeTuple(191, 'Dust & Scratches'), 131 | AttributeTuple(183, 'Slightly Wrinkled'), 132 | AttributeTuple(170, 'Scuffed Up'), 133 | AttributeTuple(164, 'Torn & Taped'), 134 | AttributeTuple(148, 'TSA Sticker'), 135 | AttributeTuple(118, 'Postage Sticker'), 136 | AttributeTuple(157, 'Scribble 2'), 137 | AttributeTuple(121, 'Barcode Sticker'), 138 | AttributeTuple(113, 'Time Flies'), 139 | AttributeTuple(117, 'Clearance Sticker'), 140 | AttributeTuple(120, 'Item Label'), 141 | AttributeTuple(151, 'Record Sticker'), 142 | AttributeTuple(144, 'Monday'), 143 | AttributeTuple(149, 'Used Sticker'), 144 | AttributeTuple(112, 'Cutouts 2'), 145 | AttributeTuple(114, 'There'), 146 | AttributeTuple(116, 'Dossier Cut Outs'), 147 | AttributeTuple(153, 'Abstract Lines'), 148 | AttributeTuple(119, 'Special Sticker'), 149 | AttributeTuple(150, 'Bora Bora'), 150 | AttributeTuple(123, 'Alphabet'), 151 | AttributeTuple(124, 'Scribble 3'), 152 | AttributeTuple(155, 'Border Accents'), 153 | AttributeTuple(154, 'Sphynx'), 154 | AttributeTuple(125, 'Scribble 1'), 155 | AttributeTuple(115, 'SQR'), 156 | AttributeTuple(111, 'Cutouts 1'), 157 | AttributeTuple(145, 'Here'), 158 | AttributeTuple(146, 'Pointless Wayfinder'), 159 | AttributeTuple(122, 'Yellow Sticker'), 160 | AttributeTuple(156, 'Incomplete Infographic'), 161 | AttributeTuple(152, 'Shredded Paper'), 162 | AttributeTuple(147, 'Merch Sticker'), 163 | AttributeTuple(107, 'Chain-Links'), 164 | AttributeTuple(104, 'Weird Fruits'), 165 | AttributeTuple(143, 'Cutouts 3'), 166 | AttributeTuple(135, 'Floating Cactus'), 167 | AttributeTuple(140, 'Favorite Number'), 168 | AttributeTuple(109, 'Botany'), 169 | AttributeTuple(98, 'Puddles'), 170 | AttributeTuple(100, 'Game Theory'), 171 | AttributeTuple(137, 'Zeros'), 172 | AttributeTuple(130, 'Title Page'), 173 | AttributeTuple(136, 'Warning Labels'), 174 | AttributeTuple(131, 'Musical Chairs'), 175 | AttributeTuple(108, 'Windows'), 176 | AttributeTuple(102, 'Catz'), 177 | AttributeTuple(110, 'Facial Features'), 178 | AttributeTuple(105, 'Mindless Machines'), 179 | AttributeTuple(99, 'Asymmetry'), 180 | AttributeTuple(134, 'Meat Sweats'), 181 | AttributeTuple(142, 'Factory'), 182 | AttributeTuple(139, 'I C U'), 183 | AttributeTuple(132, 'Too Many Eyes'), 184 | AttributeTuple(101, 'Floriculture'), 185 | AttributeTuple(141, 'Anatomy Class'), 186 | AttributeTuple(129, 'Rubber'), 187 | AttributeTuple(133, 'Marked'), 188 | AttributeTuple(97, 'Split'), 189 | AttributeTuple(103, 'Some Birds'), 190 | AttributeTuple(106, 'Unhinged'), 191 | AttributeTuple(138, 'Mediocre Painter'), 192 | AttributeTuple(95, 'Simple Curved Border'), 193 | AttributeTuple(92, 'Taped Edge'), 194 | AttributeTuple(94, 'Simple Border With Square'), 195 | AttributeTuple(65, 'Dossier'), 196 | AttributeTuple(79, 'Sunday'), 197 | AttributeTuple(93, 'Cyber Frame'), 198 | AttributeTuple(75, 'Sigmund Freud'), 199 | AttributeTuple(70, 'EyeCU'), 200 | AttributeTuple(80, 'Expo 86'), 201 | AttributeTuple(76, 'Form'), 202 | AttributeTuple(86, 'Collectors General Warning'), 203 | AttributeTuple(71, 'Slime Magazine'), 204 | AttributeTuple(88, 'S'), 205 | AttributeTuple(72, 'Incomplete'), 206 | AttributeTuple(81, "Shopp'd"), 207 | AttributeTuple(66, 'Ephemera'), 208 | AttributeTuple(74, 'Animal Pictures'), 209 | AttributeTuple(85, 'Sundaze'), 210 | AttributeTuple(67, 'ScamAbro'), 211 | AttributeTuple(96, 'Simple White Border'), 212 | AttributeTuple(89, 'Maps'), 213 | AttributeTuple(83, '1977'), 214 | AttributeTuple(87, 'Dissection Kit'), 215 | AttributeTuple(90, 'Photo Album'), 216 | AttributeTuple(73, 'CNSRD'), 217 | AttributeTuple(69, 'CULT'), 218 | AttributeTuple(82, 'Area'), 219 | AttributeTuple(91, 'Baked Beans'), 220 | AttributeTuple(68, 'Masterpiece'), 221 | AttributeTuple(84, 'Half Banner'), 222 | AttributeTuple(78, 'Mushroom Farm'), 223 | AttributeTuple(77, 'Razor Blade'), 224 | AttributeTuple(255, 'Slimesunday 1 of 1') 225 | ]; 226 | 227 | Attribute[] memory attributes = new Attribute[](attributeTuples.length); 228 | uint256[] memory traitIds = new uint256[](attributeTuples.length); 229 | for (uint256 i; i < attributeTuples.length; i++) { 230 | attributes[i] = Attribute( 231 | getLayerTypeStr(attributeTuples[i].traitId), 232 | attributeTuples[i].name, 233 | DisplayType.String 234 | ); 235 | traitIds[i] = attributeTuples[i].traitId; 236 | } 237 | 238 | TestnetToken token = new TestnetToken(); 239 | 240 | ImageLayerable(address(token.metadataContract())).setAttributes( 241 | traitIds, 242 | attributes 243 | ); 244 | ImageLayerable(address(token.metadataContract())).setBaseLayerURI( 245 | 'ipfs://bafybeihdhwqwskwwv3zdeousavfe5h4lbtxbqqz6yzrlgkzoui7h3smso4/' 246 | ); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /script/DeployAndConfigureProxy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Script} from 'forge-std/Script.sol'; 5 | import {TestnetToken} from '../src/implementations/TestnetToken.sol'; 6 | import {ImageLayerable} from '../src/metadata/ImageLayerable.sol'; 7 | import {Attribute} from '../src/interface/Structs.sol'; 8 | import {DisplayType} from '../src/interface/Enums.sol'; 9 | import {TransparentUpgradeableProxy} from 'openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol'; 10 | import {Solenv} from 'solenv/Solenv.sol'; 11 | 12 | contract Deploy is Script { 13 | struct AttributeTuple { 14 | uint256 traitId; 15 | string name; 16 | } 17 | 18 | function setUp() public virtual { 19 | Solenv.config(); 20 | } 21 | 22 | function getLayerTypeStr(uint256 layerId) 23 | public 24 | pure 25 | returns (string memory result) 26 | { 27 | uint256 layerType = (layerId - 1) / 32; 28 | if (layerType == 0) { 29 | result = 'Portrait'; 30 | } else if (layerType == 1) { 31 | result = 'Background'; 32 | } else if (layerType == 2) { 33 | result = 'Texture'; 34 | } else if (layerType == 5 || layerType == 6) { 35 | result = 'Border'; 36 | } else { 37 | result = 'Object'; 38 | } 39 | } 40 | 41 | function run() public { 42 | address deployer = vm.envAddress('DEPLOYER'); 43 | address admin = vm.envAddress('ADMIN'); 44 | address tokenAddress = vm.envAddress('TOKEN'); 45 | string memory defaultURI = vm.envString('DEFAULT_URI'); 46 | string memory baseLayerURI = vm.envString('BASE_LAYER_URI'); 47 | 48 | // use a separate admin account to deploy the proxy 49 | vm.startBroadcast(admin); 50 | // deploy this to have a copy of implementation logic 51 | ImageLayerable logic = new ImageLayerable( 52 | deployer, 53 | defaultURI, 54 | 1000, 55 | 1250, 56 | 'https://slimeshop.slimesunday.com/', 57 | 'Test Description' 58 | ); 59 | 60 | // deploy proxy using the logic contract, setting "deployer" addr as owner 61 | TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( 62 | address(logic), 63 | admin, 64 | abi.encodeWithSignature( 65 | 'initialize(address,string,uint256,uint256,string,string)', 66 | deployer, 67 | 'default', 68 | 1000, 69 | 1250, 70 | 'https://slimeshop.slimesunday.com/', 71 | 'Test Description' 72 | ) 73 | ); 74 | vm.stopBroadcast(); 75 | 76 | vm.startBroadcast(deployer); 77 | // configure layerable contract metadata 78 | ImageLayerable layerable = ImageLayerable(address(proxy)); 79 | layerable.setBaseLayerURI(baseLayerURI); 80 | 81 | // uint256[] memory layerIds = [] 82 | // Attribute[] memory attributes = [] 83 | // layerable.setAttributes(layerIds, attributes); 84 | 85 | // set metadata contract on token 86 | // TestnetToken token = TestnetToken(tokenAddress); 87 | // token.setMetadataContract(layerable); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /script/UpdateMetadataBaseURI.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {Script} from 'forge-std/Script.sol'; 5 | import {TestnetToken} from '../src/implementations/TestnetToken.sol'; 6 | import {ImageLayerable} from '../src/metadata/ImageLayerable.sol'; 7 | import {Attribute} from '../src/interface/Structs.sol'; 8 | import {DisplayType} from '../src/interface/Enums.sol'; 9 | import {TransparentUpgradeableProxy} from 'openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol'; 10 | import {Solenv} from 'solenv/Solenv.sol'; 11 | 12 | contract Deploy is Script { 13 | struct AttributeTuple { 14 | uint256 traitId; 15 | string name; 16 | } 17 | 18 | function setUp() public virtual { 19 | Solenv.config(); 20 | } 21 | 22 | function getLayerTypeStr(uint256 layerId) 23 | public 24 | pure 25 | returns (string memory result) 26 | { 27 | uint256 layerType = (layerId - 1) / 32; 28 | if (layerType == 0) { 29 | result = 'Portrait'; 30 | } else if (layerType == 1) { 31 | result = 'Background'; 32 | } else if (layerType == 2) { 33 | result = 'Texture'; 34 | } else if (layerType == 5 || layerType == 6) { 35 | result = 'Border'; 36 | } else { 37 | result = 'Object'; 38 | } 39 | } 40 | 41 | function run() public { 42 | Solenv.config(); 43 | 44 | address deployer = vm.envAddress('DEPLOYER'); 45 | address metadataContract = vm.envAddress('METADATA_PROXY'); 46 | string memory baseLayerURI = vm.envString('BASE_LAYER_URI'); 47 | 48 | // use a separate admin account to deploy the proxy 49 | vm.startBroadcast(deployer); 50 | // deploy this to have a copy of implementation logic 51 | ImageLayerable metadata = ImageLayerable(metadataContract); //, deployer); 52 | 53 | metadata.setBaseLayerURI(baseLayerURI); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /script/UpgradeMetadataContract.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Script} from 'forge-std/Script.sol'; 5 | import {Layerable} from '../src/metadata/Layerable.sol'; 6 | import {ImageLayerable} from '../src/metadata/ImageLayerable.sol'; 7 | import {Attribute} from '../src/interface/Structs.sol'; 8 | import {DisplayType} from '../src/interface/Enums.sol'; 9 | import {Strings} from 'openzeppelin-contracts/contracts/utils/Strings.sol'; 10 | import {TransparentUpgradeableProxy} from 'openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol'; 11 | import {Solenv} from 'solenv/Solenv.sol'; 12 | 13 | contract Deploy is Script { 14 | using Strings for uint256; 15 | 16 | struct AttributeTuple { 17 | uint256 traitId; 18 | string name; 19 | } 20 | 21 | function setUp() public virtual { 22 | Solenv.config(); 23 | } 24 | 25 | function run() public { 26 | address deployer = vm.envAddress('DEPLOYER'); 27 | address admin = vm.envAddress('ADMIN'); 28 | address proxyAddress = vm.envAddress('METADATA_PROXY'); 29 | 30 | vm.startBroadcast(admin); 31 | 32 | // deploy new logic contracts 33 | ImageLayerable logic = new ImageLayerable(deployer, '', 0, 0, '', ''); 34 | TransparentUpgradeableProxy proxy = TransparentUpgradeableProxy( 35 | payable(proxyAddress) 36 | ); 37 | 38 | // upgrade proxy to use the new logic contract 39 | proxy.upgradeTo(address(logic)); 40 | // vm.stopBroadcast(); 41 | 42 | vm.stopBroadcast(); 43 | vm.startBroadcast(deployer); 44 | ImageLayerable impl = ImageLayerable(proxyAddress); 45 | impl.setWidth(1000); 46 | impl.setHeight(1250); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scripts/boot.js: -------------------------------------------------------------------------------- 1 | const { Account, Address, BN } = require('ethereumjs-util'); 2 | const VM = require('@ethereumjs/vm').default; 3 | 4 | async function boot() { 5 | const pk = Buffer.from( 6 | '1122334455667788112233445566778811223344556677881122334455667788', 7 | 'hex' 8 | ); 9 | 10 | const accountAddress = Address.fromPrivateKey(pk); 11 | const account = Account.fromAccountData({ 12 | nonce: 0, 13 | balance: new BN(10).pow(new BN(18 + 2)), // 100 eth 14 | }); 15 | 16 | const vm = new VM(); 17 | await vm.stateManager.putAccount(accountAddress, account); 18 | 19 | return { vm, pk }; 20 | } 21 | 22 | module.exports = boot; 23 | -------------------------------------------------------------------------------- /scripts/call.js: -------------------------------------------------------------------------------- 1 | const { defaultAbiCoder, Interface } = require('@ethersproject/abi'); 2 | 3 | async function call(vm, address, abi, name, args = []) { 4 | const iface = new Interface(abi); 5 | const data = iface.encodeFunctionData(name, args); 6 | 7 | const renderResult = await vm.runCall({ 8 | to: address, 9 | caller: address, 10 | origin: address, 11 | data: Buffer.from(data.slice(2), 'hex'), 12 | }); 13 | 14 | if (renderResult.execResult.exceptionError) { 15 | throw renderResult.execResult.exceptionError; 16 | } 17 | 18 | const logs = renderResult.execResult.logs?.map(([address, topic, data]) => 19 | data.toString().replace(/\x00/g, '') 20 | ); 21 | 22 | if (logs?.length) { 23 | console.log(logs); 24 | } 25 | 26 | const results = defaultAbiCoder.decode( 27 | ['string'], 28 | renderResult.execResult.returnValue 29 | ); 30 | 31 | return results[0]; 32 | } 33 | 34 | module.exports = call; 35 | -------------------------------------------------------------------------------- /scripts/compile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const solc = require('solc'); 4 | 5 | function getSolcInput(source) { 6 | return { 7 | language: 'Solidity', 8 | sources: { 9 | [path.basename(source)]: { 10 | content: fs.readFileSync(source, 'utf8'), 11 | }, 12 | }, 13 | settings: { 14 | optimizer: { 15 | enabled: false, 16 | runs: 1, 17 | }, 18 | evmVersion: 'london', 19 | outputSelection: { 20 | '*': { 21 | '*': ['abi', 'evm.bytecode'], 22 | }, 23 | }, 24 | }, 25 | }; 26 | } 27 | 28 | function findImports(path) { 29 | try { 30 | const file = fs.existsSync(path) 31 | ? fs.readFileSync(path, 'utf8') 32 | : fs.readFileSync(require.resolve(path), 'utf8'); 33 | return { contents: file }; 34 | } catch (error) { 35 | console.error(error); 36 | return { error }; 37 | } 38 | } 39 | 40 | function compile(source) { 41 | const input = getSolcInput(source); 42 | process.chdir(path.dirname(source)); 43 | const output = JSON.parse( 44 | solc.compile(JSON.stringify(input), { import: findImports }) 45 | ); 46 | 47 | let errors = []; 48 | 49 | if (output.errors) { 50 | for (const error of output.errors) { 51 | if (error.severity === 'error') { 52 | errors.push(error.formattedMessage); 53 | } 54 | } 55 | } 56 | 57 | if (errors.length > 0) { 58 | throw new Error(errors.join('\n\n')); 59 | } 60 | 61 | const result = output.contracts[path.basename(source)]; 62 | const contractName = Object.keys(result)[0]; 63 | return { 64 | abi: result[contractName].abi, 65 | bytecode: result[contractName].evm.bytecode.object, 66 | }; 67 | } 68 | 69 | module.exports = compile; 70 | -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const { Address } = require('ethereumjs-util'); 2 | const { Transaction } = require('@ethereumjs/tx'); 3 | 4 | async function deploy(vm, pk, bytecode) { 5 | const address = Address.fromPrivateKey(pk); 6 | const account = await vm.stateManager.getAccount(address); 7 | 8 | const txData = { 9 | value: 0, 10 | gasLimit: 200_000_000_000, 11 | gasPrice: 1, 12 | data: '0x' + bytecode.toString('hex'), 13 | nonce: account.nonce, 14 | }; 15 | 16 | const tx = Transaction.fromTxData(txData).sign(pk); 17 | 18 | const deploymentResult = await vm.runTx({ tx }); 19 | 20 | if (deploymentResult.execResult.exceptionError) { 21 | throw deploymentResult.execResult.exceptionError; 22 | } 23 | 24 | return deploymentResult.createdAddress; 25 | } 26 | 27 | module.exports = deploy; 28 | -------------------------------------------------------------------------------- /scripts/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const serve = require('./serve'); 4 | const boot = require('./boot'); 5 | const call = require('./call'); 6 | const compile = require('./compile'); 7 | const deploy = require('./deploy'); 8 | 9 | const SOURCE = path.join(__dirname, '..', 'src', 'Renderer.sol'); 10 | 11 | async function main() { 12 | const { vm, pk } = await boot(); 13 | 14 | async function handler() { 15 | const { abi, bytecode } = compile(SOURCE); 16 | const address = await deploy(vm, pk, bytecode); 17 | const result = await call(vm, address, abi, 'example'); 18 | return result; 19 | } 20 | 21 | const { notify } = await serve(handler); 22 | 23 | fs.watch(path.dirname(SOURCE), notify); 24 | console.log('Watching', path.dirname(SOURCE)); 25 | console.log('Serving http://localhost:9901/'); 26 | } 27 | 28 | main(); 29 | -------------------------------------------------------------------------------- /scripts/qa.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const os = require('os'); 3 | const path = require('path'); 4 | const boot = require('./boot'); 5 | const call = require('./call'); 6 | const compile = require('./compile'); 7 | const deploy = require('./deploy'); 8 | const { DOMParser } = require('xmldom'); 9 | 10 | const SOURCE = path.join(__dirname, '..', 'src', 'Renderer.sol'); 11 | 12 | async function main() { 13 | const { vm, pk } = await boot(); 14 | const { abi, bytecode } = compile(SOURCE); 15 | const address = await deploy(vm, pk, bytecode); 16 | 17 | const tempFolder = fs.mkdtempSync(os.tmpdir()); 18 | console.log('Saving to', tempFolder); 19 | 20 | for (let i = 0; i < 256; i++) { 21 | const fileName = path.join(tempFolder, i + '.svg'); 22 | console.log('Rendering', fileName); 23 | const svg = await call(vm, address, abi, 'render', [i]); 24 | fs.writeFileSync(fileName, svg); 25 | 26 | // Throws on invalid XML 27 | new DOMParser().parseFromString(svg); 28 | } 29 | } 30 | 31 | main().catch((error) => { 32 | console.error(error); 33 | process.exit(1); 34 | }); 35 | -------------------------------------------------------------------------------- /scripts/serve.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const EventEmitter = require('events'); 3 | 4 | async function serve(handler) { 5 | const events = new EventEmitter(); 6 | 7 | function requestListener(req, res) { 8 | if (req.url === '/changes') { 9 | res.setHeader('Content-Type', 'text/event-stream'); 10 | res.writeHead(200); 11 | const sendEvent = () => res.write('event: change\ndata:\n\n'); 12 | events.on('change', sendEvent); 13 | req.on('close', () => events.off('change', sendEvent)); 14 | return; 15 | } 16 | 17 | if (req.url === '/') { 18 | res.writeHead(200); 19 | handler().then( 20 | (content) => res.end(webpage(content)), 21 | (error) => res.end(webpage(`
${error.message}
`)) 22 | ); 23 | return; 24 | } 25 | 26 | res.writeHead(404); 27 | res.end('Not found: ' + req.url); 28 | } 29 | const server = http.createServer(requestListener); 30 | await new Promise((resolve) => server.listen(9901, resolve)); 31 | 32 | return { 33 | notify: () => events.emit('change'), 34 | }; 35 | } 36 | 37 | const webpage = (content) => ` 38 | 39 | Hot Chain SVG 40 | ${content} 41 | 45 | 46 | `; 47 | 48 | module.exports = serve; 49 | -------------------------------------------------------------------------------- /src/Renderer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.11; 3 | 4 | import {svg} from 'hot-chain-svg/SVG.sol'; 5 | import {utils} from 'hot-chain-svg/Utils.sol'; 6 | import {TestToken} from './implementations/TestToken.sol'; 7 | import {PackedByteUtility} from './lib/PackedByteUtility.sol'; 8 | import {RandomTraits} from './traits/RandomTraits.sol'; 9 | import {ERC721Recipient} from '../test/util/ERC721Recipient.sol'; 10 | import {LayerType} from './interface/Enums.sol'; 11 | import {ImageLayerable} from './metadata/ImageLayerable.sol'; 12 | 13 | contract Renderer is ERC721Recipient { 14 | TestToken test; 15 | uint256[] distributions; 16 | 17 | constructor() { 18 | test = new TestToken('Token', 'test', ''); 19 | // todo: set rarities 20 | // 6 backgrounds 21 | distributions = [ 22 | uint256(42 * 256), 23 | uint256(84 * 256), 24 | uint256(126 * 256), 25 | uint256(168 * 256), 26 | uint256(210 * 256), 27 | uint256(252 * 256) 28 | ]; 29 | uint256[] memory _distributions = distributions; 30 | uint256[2] memory packedDistributions = PackedByteUtility 31 | .packArrayOfShorts(_distributions); 32 | test.setLayerTypeDistribution( 33 | uint8(LayerType.BACKGROUND), 34 | packedDistributions 35 | ); 36 | packedDistributions = [uint256(2**16) << 240, uint256(0)]; 37 | 38 | // 1 portrait 39 | test.setLayerTypeDistribution( 40 | uint8(LayerType.PORTRAIT), 41 | packedDistributions 42 | ); 43 | // 5 textures 44 | distributions = [ 45 | uint256(51 * 256), 46 | uint256(102 * 256), 47 | uint256(153 * 256), 48 | uint256(204 * 256), 49 | uint256(255 * 256) 50 | ]; 51 | _distributions = distributions; 52 | packedDistributions = PackedByteUtility.packArrayOfShorts( 53 | _distributions 54 | ); 55 | test.setLayerTypeDistribution( 56 | uint8(LayerType.TEXTURE), 57 | packedDistributions 58 | ); 59 | // 8 objects 60 | distributions = [ 61 | uint256(31 * 256), 62 | uint256(62 * 256), 63 | uint256(93 * 256), 64 | uint256(124 * 256), 65 | uint256(155 * 256), 66 | uint256(186 * 256), 67 | uint256(217 * 256), 68 | uint256(248 * 256) 69 | ]; 70 | _distributions = distributions; 71 | packedDistributions = PackedByteUtility.packArrayOfShorts( 72 | _distributions 73 | ); 74 | test.setLayerTypeDistribution( 75 | uint8(LayerType.OBJECT), 76 | packedDistributions 77 | ); 78 | // 7 borders 79 | distributions = [ 80 | uint256(36), 81 | uint256(72), 82 | uint256(108), 83 | uint256(144), 84 | uint256(180), 85 | uint256(216), 86 | uint256(252) 87 | ]; 88 | _distributions = distributions; 89 | packedDistributions = PackedByteUtility.packArrayOfShorts( 90 | _distributions 91 | ); 92 | test.setLayerTypeDistribution( 93 | uint8(LayerType.BORDER), 94 | packedDistributions 95 | ); 96 | ImageLayerable(address(test.metadataContract())).setBaseLayerURI( 97 | '/Users/jameswenzel/dev/partner-smart-contracts/Layers/' 98 | ); 99 | } 100 | 101 | function render(uint256 tokenId) public returns (string memory) { 102 | test.mintSet(); 103 | uint256 startingTokenId = tokenId * 7; 104 | // get layerIds from token IDs 105 | uint256[] memory layers = new uint256[](7); 106 | for ( 107 | uint256 layerTokenId = startingTokenId; 108 | layerTokenId < startingTokenId + 7; 109 | layerTokenId++ 110 | ) { 111 | uint256 layer = test.getLayerId(layerTokenId); 112 | uint256 lastLayer = 0; 113 | if (layerTokenId > startingTokenId) { 114 | lastLayer = layers[(layerTokenId % 7) - 1]; 115 | } 116 | if (layer == lastLayer) { 117 | layer += 1; 118 | } 119 | if (layer == 2) { 120 | layer = 1; 121 | } 122 | layers[layerTokenId % 7] = uint256(layer); 123 | } 124 | // create copy as uint256 bc todo: i need to fix 125 | // uint256[] memory packedLayers = PackedByteUtility.packArrayOfShorts(layers); 126 | // unpack layerIDs into a binding - todo: make this a public function idk 127 | // uint256 binding = test.packedLayersToBitMap(packedLayers); 128 | // test.bindLayers(startingTokenId, binding); 129 | // swap layer ordering 130 | uint256 temp = layers[0]; 131 | layers[0] = layers[1]; 132 | layers[1] = temp; 133 | // uint256[] memory newPackedLayers = PackedByteUtility.packArrayOfShorts( 134 | // layers 135 | // ); 136 | // set active layers - use portrait id, not b 137 | // test.setActiveLayers(startingTokenId, newPackedLayers); 138 | return test.metadataContract().getLayeredTokenImageURI(layers); 139 | } 140 | 141 | function example() external returns (string memory) { 142 | return render(0); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/SVG.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.12; 3 | 4 | import {utils} from './Utils.sol'; 5 | 6 | // Core SVG utilitiy library which helps us construct 7 | // onchain SVG's with a simple, web-like API. 8 | library svg { 9 | /* MAIN ELEMENTS */ 10 | function g(string memory _props, string memory _children) 11 | internal 12 | pure 13 | returns (string memory) 14 | { 15 | return el('g', _props, _children); 16 | } 17 | 18 | function path(string memory _props, string memory _children) 19 | internal 20 | pure 21 | returns (string memory) 22 | { 23 | return el('path', _props, _children); 24 | } 25 | 26 | function text(string memory _props, string memory _children) 27 | internal 28 | pure 29 | returns (string memory) 30 | { 31 | return el('text', _props, _children); 32 | } 33 | 34 | function line(string memory _props, string memory _children) 35 | internal 36 | pure 37 | returns (string memory) 38 | { 39 | return el('line', _props, _children); 40 | } 41 | 42 | function circle(string memory _props, string memory _children) 43 | internal 44 | pure 45 | returns (string memory) 46 | { 47 | return el('circle', _props, _children); 48 | } 49 | 50 | function circle(string memory _props) 51 | internal 52 | pure 53 | returns (string memory) 54 | { 55 | return el('circle', _props); 56 | } 57 | 58 | function rect(string memory _props, string memory _children) 59 | internal 60 | pure 61 | returns (string memory) 62 | { 63 | return el('rect', _props, _children); 64 | } 65 | 66 | function rect(string memory _props) internal pure returns (string memory) { 67 | return el('rect', _props); 68 | } 69 | 70 | function filter(string memory _props, string memory _children) 71 | internal 72 | pure 73 | returns (string memory) 74 | { 75 | return el('filter', _props, _children); 76 | } 77 | 78 | function cdata(string memory _content) 79 | internal 80 | pure 81 | returns (string memory) 82 | { 83 | return string.concat(''); 84 | } 85 | 86 | /* GRADIENTS */ 87 | function radialGradient(string memory _props, string memory _children) 88 | internal 89 | pure 90 | returns (string memory) 91 | { 92 | return el('radialGradient', _props, _children); 93 | } 94 | 95 | function linearGradient(string memory _props, string memory _children) 96 | internal 97 | pure 98 | returns (string memory) 99 | { 100 | return el('linearGradient', _props, _children); 101 | } 102 | 103 | function gradientStop( 104 | uint256 offset, 105 | string memory stopColor, 106 | string memory _props 107 | ) internal pure returns (string memory) { 108 | return 109 | el( 110 | 'stop', 111 | string.concat( 112 | prop('stop-color', stopColor), 113 | ' ', 114 | prop('offset', string.concat(utils.uint2str(offset), '%')), 115 | ' ', 116 | _props 117 | ) 118 | ); 119 | } 120 | 121 | function animateTransform(string memory _props) 122 | internal 123 | pure 124 | returns (string memory) 125 | { 126 | return el('animateTransform', _props); 127 | } 128 | 129 | function image(string memory _href, string memory _props) 130 | internal 131 | pure 132 | returns (string memory) 133 | { 134 | return el('image', string.concat(prop('href', _href), ' ', _props)); 135 | } 136 | 137 | /* COMMON */ 138 | // A generic element, can be used to construct any SVG (or HTML) element 139 | function el( 140 | string memory _tag, 141 | string memory _props, 142 | string memory _children 143 | ) internal pure returns (string memory) { 144 | return 145 | string.concat( 146 | '<', 147 | _tag, 148 | ' ', 149 | _props, 150 | '>', 151 | _children, 152 | '' 155 | ); 156 | } 157 | 158 | // A generic element, can be used to construct any SVG (or HTML) element without children 159 | function el(string memory _tag, string memory _props) 160 | internal 161 | pure 162 | returns (string memory) 163 | { 164 | return string.concat('<', _tag, ' ', _props, '/>'); 165 | } 166 | 167 | // an SVG attribute 168 | function prop(string memory _key, string memory _val) 169 | internal 170 | pure 171 | returns (string memory) 172 | { 173 | return string.concat(_key, '=', '"', _val, '" '); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Utils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.12; 3 | 4 | // Core utils used extensively to format CSS and numbers. 5 | library utils { 6 | // used to simulate empty strings 7 | string internal constant NULL = ''; 8 | 9 | // formats a CSS variable line. includes a semicolon for formatting. 10 | function setCssVar(string memory _key, string memory _val) 11 | internal 12 | pure 13 | returns (string memory) 14 | { 15 | return string.concat('--', _key, ':', _val, ';'); 16 | } 17 | 18 | // formats getting a css variable 19 | function getCssVar(string memory _key) 20 | internal 21 | pure 22 | returns (string memory) 23 | { 24 | return string.concat('var(--', _key, ')'); 25 | } 26 | 27 | // formats getting a def URL 28 | function getDefURL(string memory _id) 29 | internal 30 | pure 31 | returns (string memory) 32 | { 33 | return string.concat('url(#', _id, ')'); 34 | } 35 | 36 | // formats rgba white with a specified opacity / alpha 37 | function white_a(uint256 _a) internal pure returns (string memory) { 38 | return rgba(255, 255, 255, _a); 39 | } 40 | 41 | // formats rgba black with a specified opacity / alpha 42 | function black_a(uint256 _a) internal pure returns (string memory) { 43 | return rgba(0, 0, 0, _a); 44 | } 45 | 46 | // formats generic rgba color in css 47 | function rgba( 48 | uint256 _r, 49 | uint256 _g, 50 | uint256 _b, 51 | uint256 _a 52 | ) internal pure returns (string memory) { 53 | string memory formattedA = _a < 100 54 | ? string.concat('0.', utils.uint2str(_a)) 55 | : '1'; 56 | return 57 | string.concat( 58 | 'rgba(', 59 | utils.uint2str(_r), 60 | ',', 61 | utils.uint2str(_g), 62 | ',', 63 | utils.uint2str(_b), 64 | ',', 65 | formattedA, 66 | ')' 67 | ); 68 | } 69 | 70 | // checks if two strings are equal 71 | function stringsEqual(string memory _a, string memory _b) 72 | internal 73 | pure 74 | returns (bool) 75 | { 76 | return keccak256(bytes(_a)) == keccak256(bytes(_b)); 77 | } 78 | 79 | // returns the length of a string in characters 80 | function utfStringLength(string memory _str) 81 | internal 82 | pure 83 | returns (uint256 length) 84 | { 85 | uint256 i = 0; 86 | bytes memory string_rep = bytes(_str); 87 | 88 | while (i < string_rep.length) { 89 | if (string_rep[i] >> 7 == 0) i += 1; 90 | else if (string_rep[i] >> 5 == bytes1(uint8(0x6))) i += 2; 91 | else if (string_rep[i] >> 4 == bytes1(uint8(0xE))) i += 3; 92 | else if (string_rep[i] >> 3 == bytes1(uint8(0x1E))) 93 | i += 4; //For safety 94 | else i += 1; 95 | 96 | length++; 97 | } 98 | } 99 | 100 | // converts an unsigned integer to a string 101 | function uint2str(uint256 _i) 102 | internal 103 | pure 104 | returns (string memory _uintAsString) 105 | { 106 | if (_i == 0) { 107 | return '0'; 108 | } 109 | uint256 j = _i; 110 | uint256 len; 111 | while (j != 0) { 112 | len++; 113 | j /= 10; 114 | } 115 | bytes memory bstr = new bytes(len); 116 | uint256 k = len; 117 | while (_i != 0) { 118 | k = k - 1; 119 | uint8 temp = 48 + uint8(_i - (_i / 10) * 10); 120 | bytes1 b1 = bytes1(temp); 121 | bstr[k] = b1; 122 | _i /= 10; 123 | } 124 | return string(bstr); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/examples/BoundLayerableFirstComposedCutoff.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {BoundLayerable} from '../BoundLayerable.sol'; 5 | 6 | /** 7 | * @notice BoundLayerable contract that automatically binds a special layer if composed (layers are bound) 8 | * before the cutoff time 9 | */ 10 | abstract contract BoundLayerableFirstComposedCutoff is BoundLayerable { 11 | uint256 immutable FIRST_COMPOSED_CUTOFF; 12 | uint8 immutable EXCLUSIVE_LAYER_ID; 13 | 14 | constructor( 15 | string memory name, 16 | string memory symbol, 17 | address vrfCoordinatorAddress, 18 | uint240 maxNumSets, 19 | uint8 numTokensPerSet, 20 | uint64 subscriptionId, 21 | address metadataContractAddress, 22 | uint256 firstComposedCutoff, 23 | uint8 exclusiveLayerId, 24 | uint8 numRandomBatches, 25 | bytes32 keyHash 26 | ) 27 | BoundLayerable( 28 | name, 29 | symbol, 30 | vrfCoordinatorAddress, 31 | maxNumSets, 32 | numTokensPerSet, 33 | subscriptionId, 34 | metadataContractAddress, 35 | numRandomBatches, 36 | keyHash 37 | ) 38 | { 39 | FIRST_COMPOSED_CUTOFF = firstComposedCutoff; 40 | EXCLUSIVE_LAYER_ID = exclusiveLayerId; 41 | } 42 | 43 | function _setBoundLayersAndEmitEvent(uint256 baseTokenId, uint256 bindings) 44 | internal 45 | virtual 46 | override 47 | { 48 | // automatically bind a special layer if the base token was composed before the cutoff time 49 | uint256 exclusiveLayerId = EXCLUSIVE_LAYER_ID; 50 | uint256 firstComposedCutoff = FIRST_COMPOSED_CUTOFF; 51 | /// @solidity memory-safe-assembly 52 | assembly { 53 | // conditionally set the exclusive layer bit if the base token is composed before cutoff 54 | bindings := or( 55 | bindings, 56 | shl( 57 | exclusiveLayerId, 58 | // 1 if timestamp is before cutoff, 0 otherwise (ie, no-op) 59 | lt(timestamp(), firstComposedCutoff) 60 | ) 61 | ) 62 | } 63 | super._setBoundLayersAndEmitEvent(baseTokenId, bindings); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/implementations/BoundLayerableFirstComposedCutoffImpl.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {BoundLayerable} from 'bound-layerable/BoundLayerable.sol'; 5 | 6 | import {BoundLayerableFirstComposedCutoff} from 'bound-layerable/examples/BoundLayerableFirstComposedCutoff.sol'; 7 | import {LayerVariation} from 'bound-layerable/interface/Structs.sol'; 8 | import {ImageLayerable} from 'bound-layerable/metadata/ImageLayerable.sol'; 9 | import {LayerType} from 'bound-layerable/interface/Enums.sol'; 10 | import {RandomTraitsImpl} from 'bound-layerable/traits/RandomTraitsImpl.sol'; 11 | import {RandomTraits} from 'bound-layerable/traits/RandomTraits.sol'; 12 | import {MAX_INT} from 'bound-layerable/interface/Constants.sol'; 13 | import {ERC721A} from 'bound-layerable/token/ERC721A.sol'; 14 | import {PackedByteUtility} from 'bound-layerable/lib/PackedByteUtility.sol'; 15 | 16 | contract BoundLayerableFirstComposedCutoffImpl is 17 | BoundLayerableFirstComposedCutoff, 18 | RandomTraitsImpl 19 | { 20 | constructor() 21 | BoundLayerableFirstComposedCutoff( 22 | '', 23 | '', 24 | address(1234), 25 | 5555, 26 | 7, 27 | 1, 28 | address(0), 29 | 2**32, 30 | 255, 31 | 16, 32 | bytes32(uint256(1)) 33 | ) 34 | { 35 | for (uint256 i; i < 8; ++i) { 36 | uint256[2] memory dists = [uint256(0), uint256(0)]; 37 | for (uint256 k; k < 2; ++k) { 38 | uint256 dist = dists[k]; 39 | for (uint256 j; j < 16; ++j) { 40 | uint256 short = (j + 1 + (16 * k)) * 2047; 41 | dist = PackedByteUtility.packShortAtIndex(dist, short, j); 42 | dists[k] = dist; 43 | } 44 | } 45 | layerTypeToPackedDistributions[getLayerType(i)] = dists; 46 | } 47 | metadataContract = new ImageLayerable( 48 | msg.sender, 49 | 'default', 50 | 100, 51 | 100, 52 | 'external', 53 | 'description' 54 | ); 55 | } 56 | 57 | function setPackedBatchRandomness(bytes32 seed) public { 58 | packedBatchRandomness = seed; 59 | } 60 | 61 | function mint() public { 62 | _setPlaceholderBinding(_nextTokenId()); 63 | super._mint(msg.sender, 7); 64 | } 65 | 66 | function setBoundLayers(uint256 tokenId, uint256 bindings) public { 67 | _tokenIdToBoundLayers[tokenId] = bindings; 68 | } 69 | 70 | function setBoundLayersBulk( 71 | uint256[] calldata tokenIds, 72 | uint256[] calldata bindings 73 | ) public { 74 | for (uint256 i = 0; i < tokenIds.length; ++i) { 75 | _tokenIdToBoundLayers[tokenIds[i]] = bindings[i]; 76 | } 77 | } 78 | 79 | function getLayerType(uint256 tokenId) 80 | public 81 | view 82 | virtual 83 | override(RandomTraits, RandomTraitsImpl) 84 | returns (uint8) 85 | { 86 | return RandomTraitsImpl.getLayerType(tokenId); 87 | } 88 | 89 | function checkUnpackedIsSubsetOfBound( 90 | uint256 unpackedLayers, 91 | uint256 boundLayers 92 | ) public pure virtual { 93 | _checkUnpackedIsSubsetOfBound(unpackedLayers, boundLayers); 94 | } 95 | 96 | function unpackLayersToBitMapAndCheckForDuplicates(uint256 packedLayers) 97 | public 98 | virtual 99 | returns (uint256, uint256) 100 | { 101 | return _unpackLayersToBitMapAndCheckForDuplicates(packedLayers); 102 | } 103 | 104 | function getActiveLayersRaw(uint256 tokenId) 105 | public 106 | view 107 | returns (uint256 activeLayers) 108 | { 109 | return _tokenIdToPackedActiveLayers[tokenId]; 110 | } 111 | 112 | function getLayerId(uint256 tokenId) 113 | public 114 | view 115 | override 116 | returns (uint256) 117 | { 118 | super.getLayerId(tokenId); 119 | return tokenId + 1; 120 | } 121 | 122 | function getLayerId(uint256 tokenId, bytes32 seed) 123 | internal 124 | view 125 | override 126 | returns (uint256) 127 | { 128 | super.getLayerId(tokenId, seed); 129 | return tokenId + 1; 130 | } 131 | 132 | function isBurned(uint256 tokenId) public view returns (bool) { 133 | return _isBurned(tokenId); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/implementations/BoundLayerableSnapshotImpl.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {BoundLayerable} from 'bound-layerable/BoundLayerable.sol'; 5 | import {LayerVariation} from 'bound-layerable/interface/Structs.sol'; 6 | import {ImageLayerable} from 'bound-layerable/metadata/ImageLayerable.sol'; 7 | import {LayerType} from 'bound-layerable/interface/Enums.sol'; 8 | import {RandomTraitsImpl} from 'bound-layerable/traits/RandomTraitsImpl.sol'; 9 | import {RandomTraits} from 'bound-layerable/traits/RandomTraits.sol'; 10 | import {MAX_INT} from 'bound-layerable/interface/Constants.sol'; 11 | import {PackedByteUtility} from 'bound-layerable/lib/PackedByteUtility.sol'; 12 | 13 | contract BoundLayerableSnapshotImpl is BoundLayerable, RandomTraitsImpl { 14 | constructor() 15 | BoundLayerable( 16 | '', 17 | '', 18 | address(1234), 19 | 5555, 20 | 7, 21 | 1, 22 | address(0), 23 | 16, 24 | bytes32(uint256(1)) 25 | ) 26 | { 27 | packedBatchRandomness = bytes32(uint256(1)); 28 | for (uint256 i; i < 8; ++i) { 29 | uint256[2] memory dists = [uint256(0), uint256(0)]; 30 | for (uint256 k; k < 2; ++k) { 31 | uint256 dist = dists[k]; 32 | for (uint256 j; j < 16; ++j) { 33 | uint256 short = (j + 1 + (16 * k)) * 2047; 34 | dist = PackedByteUtility.packShortAtIndex(dist, short, j); 35 | dists[k] = dist; 36 | } 37 | } 38 | layerTypeToPackedDistributions[getLayerType(i)] = dists; 39 | } 40 | 41 | metadataContract = new ImageLayerable( 42 | msg.sender, 43 | 'default', 44 | 100, 45 | 100, 46 | 'external', 47 | 'description' 48 | ); 49 | } 50 | 51 | function setPackedBatchRandomness(bytes32 seed) public { 52 | packedBatchRandomness = seed; 53 | } 54 | 55 | function mint() public { 56 | _setPlaceholderBinding(_nextTokenId()); 57 | _setPlaceholderActiveLayers(_nextTokenId()); 58 | super._mint(msg.sender, 7); 59 | } 60 | 61 | function mint(uint256 numSets) public { 62 | super._mint(msg.sender, 7 * numSets); 63 | } 64 | 65 | function setBoundLayers(uint256 tokenId, uint256 bindings) public { 66 | _tokenIdToBoundLayers[tokenId] = bindings; 67 | } 68 | 69 | function setBoundLayersBulk( 70 | uint256[] calldata tokenIds, 71 | uint256[] calldata bindings 72 | ) public { 73 | for (uint256 i = 0; i < tokenIds.length; ++i) { 74 | _tokenIdToBoundLayers[tokenIds[i]] = bindings[i]; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/implementations/BoundLayerableTestImpl.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {BoundLayerableSnapshotImpl} from './BoundLayerableSnapshotImpl.sol'; 5 | import {RandomTraitsImpl} from 'bound-layerable/traits/RandomTraitsImpl.sol'; 6 | import {LayerVariation} from 'bound-layerable/interface/Structs.sol'; 7 | import {ImageLayerable} from 'bound-layerable/metadata/ImageLayerable.sol'; 8 | import {LayerType} from 'bound-layerable/interface/Enums.sol'; 9 | import {RandomTraits} from 'bound-layerable/traits/RandomTraits.sol'; 10 | 11 | contract BoundLayerableTestImpl is BoundLayerableSnapshotImpl { 12 | function getLayerType(uint256 tokenId) 13 | public 14 | view 15 | virtual 16 | override(RandomTraits, RandomTraitsImpl) 17 | returns (uint8) 18 | { 19 | return RandomTraitsImpl.getLayerType(tokenId); 20 | } 21 | 22 | function checkUnpackedIsSubsetOfBound( 23 | uint256 unpackedLayers, 24 | uint256 boundLayers 25 | ) public pure virtual { 26 | _checkUnpackedIsSubsetOfBound(unpackedLayers, boundLayers); 27 | } 28 | 29 | function unpackLayersToBitMapAndCheckForDuplicates(uint256 packedLayers) 30 | public 31 | virtual 32 | returns (uint256, uint256) 33 | { 34 | return _unpackLayersToBitMapAndCheckForDuplicates(packedLayers); 35 | } 36 | 37 | function getActiveLayersRaw(uint256 tokenId) 38 | public 39 | view 40 | returns (uint256 activeLayers) 41 | { 42 | return _tokenIdToPackedActiveLayers[tokenId]; 43 | } 44 | 45 | function getLayerId(uint256 tokenId) 46 | public 47 | view 48 | override 49 | returns (uint256) 50 | { 51 | super.getLayerId(tokenId); 52 | return tokenId + 1; 53 | } 54 | 55 | function getLayerId(uint256 tokenId, bytes32 seed) 56 | internal 57 | view 58 | override 59 | returns (uint256) 60 | { 61 | super.getLayerId(tokenId, seed); 62 | return tokenId + 1; 63 | } 64 | 65 | function isBurned(uint256 tokenId) public view returns (bool) { 66 | return _isBurned(tokenId); 67 | } 68 | 69 | function getTokenURI(uint256) public view returns (string memory) { 70 | return _tokenURI(0); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/implementations/TestToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {ERC721A} from '../token/ERC721A.sol'; 5 | 6 | import {BoundLayerableTestImpl} from './BoundLayerableTestImpl.sol'; 7 | import {RandomTraits} from '../traits/RandomTraits.sol'; 8 | import {json} from '../lib/JSON.sol'; 9 | import '../interface/Errors.sol'; 10 | 11 | contract TestToken is BoundLayerableTestImpl { 12 | uint256 public constant MAX_SUPPLY = 5555; 13 | uint256 public constant MINT_PRICE = 0 ether; 14 | bool private tradingActive = true; 15 | 16 | // TODO: disable transferring to someone who does not own a base layer? 17 | constructor( 18 | string memory name, 19 | string memory symbol, 20 | string memory defaultURI 21 | ) {} 22 | 23 | modifier includesCorrectPayment(uint256 _numSets) { 24 | if (msg.value != _numSets * MINT_PRICE) { 25 | revert IncorrectPayment(); 26 | } 27 | _; 28 | } 29 | 30 | function disableTrading() external onlyOwner { 31 | if (!tradingActive) { 32 | revert TradingAlreadyDisabled(); 33 | } 34 | // todo: break this out if it will hit gas limit 35 | _burnLayers(); 36 | // this will free up some gas! 37 | tradingActive = false; 38 | } 39 | 40 | function _burnLayers() private { 41 | // iterate over all token ids 42 | for (uint256 i; i < MAX_SUPPLY; ) { 43 | if (i % 7 != 0) { 44 | // "burn" layer by emitting transfer event to null address 45 | // note: can't use bulktransfer bc no guarantee that all layers are owned by same address 46 | // emit Transfer(owner_, address(0), i); 47 | _burn(i); 48 | } 49 | unchecked { 50 | ++i; 51 | } 52 | } 53 | } 54 | 55 | function tokenURI(uint256 tokenId) 56 | public 57 | view 58 | virtual 59 | override(ERC721A) 60 | returns (string memory) 61 | { 62 | return _tokenURI(tokenId); 63 | } 64 | 65 | function ownerOf(uint256) public view override returns (address) { 66 | return msg.sender; 67 | } 68 | 69 | function mintSet() public payable includesCorrectPayment(1) { 70 | // TODO: test this does not mess with active layers etc 71 | _setPlaceholderBinding(_nextTokenId()); 72 | super._mint(msg.sender, 7); 73 | } 74 | 75 | // todo: restrict numminted 76 | function mintSets(uint256 numSets) 77 | public 78 | payable 79 | includesCorrectPayment(numSets) 80 | { 81 | super._mint(msg.sender, 7 * numSets); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/implementations/TestnetToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {BoundLayerable} from '../BoundLayerable.sol'; 5 | import {RandomTraitsImpl} from '../traits/RandomTraitsImpl.sol'; 6 | import {IncorrectPayment} from '../interface/Errors.sol'; 7 | import {ERC721A} from '../token/ERC721A.sol'; 8 | import {ImageLayerable} from '../metadata/ImageLayerable.sol'; 9 | 10 | contract TestnetToken is BoundLayerable, RandomTraitsImpl { 11 | uint256 public constant MINT_PRICE = 0 ether; 12 | 13 | constructor() 14 | BoundLayerable( 15 | 'test', 16 | 'TEST', 17 | 0x6168499c0cFfCaCD319c818142124B7A15E857ab, 18 | 1000, 19 | 7, 20 | 8632, 21 | address(0), 22 | 16, 23 | bytes32(uint256(1)) 24 | ) 25 | { 26 | // metadataContract = new ImageLayerable('default', msg.sender); 27 | } 28 | 29 | modifier includesCorrectPayment(uint256 numSets) { 30 | if (msg.value != numSets * MINT_PRICE) { 31 | revert IncorrectPayment(); 32 | } 33 | _; 34 | } 35 | 36 | function tokenURI(uint256 tokenId) 37 | public 38 | view 39 | virtual 40 | override(ERC721A) 41 | returns (string memory) 42 | { 43 | return _tokenURI(tokenId); 44 | } 45 | 46 | function mintSet() public payable includesCorrectPayment(1) { 47 | _setPlaceholderBinding(_nextTokenId()); 48 | super._mint(msg.sender, NUM_TOKENS_PER_SET); 49 | } 50 | 51 | // todo: restrict numminted 52 | function mintSets(uint256 numSets) 53 | public 54 | payable 55 | includesCorrectPayment(numSets) 56 | { 57 | super._mint(msg.sender, NUM_TOKENS_PER_SET * numSets); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/interface/Constants.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | uint256 constant NOT_0TH_BITMASK = 2**256 - 2; 5 | uint256 constant MAX_INT = 2**256 - 1; 6 | uint136 constant _2_128 = 2**128; 7 | uint72 constant _2_64 = 2**64; 8 | uint40 constant _2_32 = 2**32; 9 | uint24 constant _2_16 = 2**16; 10 | uint16 constant _2_8 = 2**8; 11 | uint8 constant _2_4 = 2**4; 12 | uint8 constant _2_2 = 2**2; 13 | uint8 constant _2_1 = 2**1; 14 | 15 | uint128 constant _128_MASK = 2**128 - 1; 16 | uint64 constant _64_MASK = 2**64 - 1; 17 | uint32 constant _32_MASK = 2**32 - 1; 18 | uint16 constant _16_MASK = 2**16 - 1; 19 | uint8 constant _8_MASK = 2**8 - 1; 20 | uint8 constant _4_MASK = 2**4 - 1; 21 | uint8 constant _2_MASK = 2**2 - 1; 22 | uint8 constant _1_MASK = 2**1 - 1; 23 | 24 | bytes4 constant DUPLICATE_ACTIVE_LAYERS_SIGNATURE = 0x6411ce75; 25 | bytes4 constant LAYER_NOT_BOUND_TO_TOKEN_ID_SIGNATURE = 0xa385f805; 26 | bytes4 constant BAD_DISTRIBUTIONS_SIGNATURE = 0x338096f7; 27 | bytes4 constant MULTIPLE_VARIATIONS_ENABLED_SIGNATURE = 0x4d2e9396; 28 | bytes4 constant BATCH_NOT_REVEALED_SIGNATURE = 0x729b0f75; 29 | -------------------------------------------------------------------------------- /src/interface/Enums.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | enum DisplayType { 5 | String, 6 | Number, 7 | Date, 8 | BoostPercent, 9 | BoostNumber 10 | } 11 | 12 | // TODO: generalize this, probably uint8s 13 | enum LayerType { 14 | PORTRAIT, 15 | BACKGROUND, 16 | TEXTURE, 17 | OBJECT, 18 | OBJECT2, 19 | BORDER 20 | } 21 | -------------------------------------------------------------------------------- /src/interface/Errors.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | error TradingAlreadyDisabled(); 5 | error IncorrectPayment(); 6 | error ArrayLengthMismatch(uint256 length1, uint256 length2); 7 | error LayerNotBoundToTokenId(); 8 | error DuplicateActiveLayers(); 9 | error MultipleVariationsEnabled(); 10 | error InvalidLayer(uint256 layer); 11 | error BadDistributions(); 12 | error NotOwner(); 13 | error BatchNotRevealed(); 14 | error LayerAlreadyBound(); 15 | error CannotBindBase(); 16 | error OnlyBase(); 17 | error InvalidLayerType(); 18 | error MaxSupply(); 19 | error MaxRandomness(); 20 | error OnlyCoordinatorCanFulfill(address have, address want); 21 | error UnsafeReveal(); 22 | error NoActiveLayers(); 23 | error InvalidInitialization(); 24 | error NumRandomBatchesMustBePowerOfTwo(); 25 | error NumRandomBatchesMustBeGreaterThanOne(); 26 | error NumRandomBatchesMustBeLessThanOrEqualTo16(); 27 | error RevealPending(); 28 | error NoBatchesToReveal(); 29 | -------------------------------------------------------------------------------- /src/interface/Events.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | interface BoundLayerableEvents { 5 | event LayersBoundToToken( 6 | address indexed owner, 7 | uint256 indexed tokenId, 8 | uint256 indexed boundLayersBitmap 9 | ); 10 | 11 | event ActiveLayersChanged( 12 | address indexed owner, 13 | uint256 indexed tokenId, 14 | uint256 indexed activeLayersBytearray 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/interface/Structs.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {DisplayType} from './Enums.sol'; 5 | 6 | struct Attribute { 7 | string traitType; 8 | string value; 9 | DisplayType displayType; 10 | } 11 | 12 | // TODO: just pack these into a uint256 bytearray 13 | struct LayerVariation { 14 | uint8 layerId; 15 | uint8 numVariations; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/BitMapUtility.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import '../interface/Constants.sol'; 5 | 6 | library BitMapUtility { 7 | /** 8 | * @notice Convert a byte value into a bitmap, where the bit at position val is set to 1, and all others 0 9 | * @param val byte value to convert to bitmap 10 | * @return bitmap of val 11 | */ 12 | function toBitMap(uint256 val) internal pure returns (uint256 bitmap) { 13 | /// @solidity memory-safe-assembly 14 | assembly { 15 | bitmap := shl(val, 1) 16 | } 17 | } 18 | 19 | /** 20 | * @notice get the intersection of two bitMaps by ANDing them together 21 | * @param target first bitmap 22 | * @param test second bitmap 23 | * @return result bitmap with only bits active in both bitmaps set to 1 24 | */ 25 | function intersect(uint256 target, uint256 test) 26 | internal 27 | pure 28 | returns (uint256 result) 29 | { 30 | /// @solidity memory-safe-assembly 31 | assembly { 32 | result := and(target, test) 33 | } 34 | } 35 | 36 | /** 37 | * @notice check if bitmap has byteVal set to 1 38 | * @param target first bitmap 39 | * @param byteVal bit position to check in target 40 | * @return result true if bitmap contains byteVal 41 | */ 42 | function contains(uint256 target, uint256 byteVal) 43 | internal 44 | pure 45 | returns (bool result) 46 | { 47 | /// @solidity memory-safe-assembly 48 | assembly { 49 | result := and(shr(byteVal, target), 1) 50 | } 51 | } 52 | 53 | /** 54 | * @notice check if union of two bitmaps is equal to the first 55 | * @param superset first bitmap 56 | * @param subset second bitmap 57 | * @return result true if superset is a superset of subset, false otherwise 58 | */ 59 | function isSupersetOf(uint256 superset, uint256 subset) 60 | internal 61 | pure 62 | returns (bool result) 63 | { 64 | /// @solidity memory-safe-assembly 65 | assembly { 66 | result := eq(superset, or(superset, subset)) 67 | } 68 | } 69 | 70 | /** 71 | * @notice unpack a bitmap into an array of included byte values 72 | * @param bitMap bitMap to unpack into byte values 73 | * @return unpacked array of byte values included in bitMap, sorted from smallest to largest 74 | */ 75 | function unpackBitMap(uint256 bitMap) 76 | internal 77 | pure 78 | returns (uint256[] memory unpacked) 79 | { 80 | /// @solidity memory-safe-assembly 81 | assembly { 82 | if iszero(bitMap) { 83 | let freePtr := mload(0x40) 84 | mstore(0x40, add(freePtr, 0x20)) 85 | return(freePtr, 0x20) 86 | } 87 | function lsb(x) -> r { 88 | x := and(x, add(not(x), 1)) 89 | r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) 90 | r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) 91 | r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) 92 | 93 | x := shr(r, x) 94 | x := or(x, shr(1, x)) 95 | x := or(x, shr(2, x)) 96 | x := or(x, shr(4, x)) 97 | x := or(x, shr(8, x)) 98 | x := or(x, shr(16, x)) 99 | 100 | r := or( 101 | r, 102 | byte( 103 | and(31, shr(27, mul(x, 0x07C4ACDD))), 104 | 0x0009010a0d15021d0b0e10121619031e080c141c0f111807131b17061a05041f 105 | ) 106 | ) 107 | } 108 | 109 | // set unpacked ptr to free mem 110 | unpacked := mload(0x40) 111 | // get ptr to first index of array 112 | let unpackedIndexPtr := add(unpacked, 0x20) 113 | 114 | let numLayers 115 | for { 116 | 117 | } bitMap { 118 | unpackedIndexPtr := add(unpackedIndexPtr, 0x20) 119 | } { 120 | // store the index of the lsb at the index in the array 121 | mstore(unpackedIndexPtr, lsb(bitMap)) 122 | // drop the lsb from the bitMap 123 | bitMap := and(bitMap, sub(bitMap, 1)) 124 | // increment numLayers 125 | numLayers := add(numLayers, 1) 126 | } 127 | // store the number of layers at the pointer to unpacked array 128 | mstore(unpacked, numLayers) 129 | // update free mem pointer to first free slot after unpacked array 130 | mstore(0x40, unpackedIndexPtr) 131 | } 132 | } 133 | 134 | /** 135 | * @notice pack an array of byte values into a bitmap 136 | * @param uints array of byte values to pack into bitmap 137 | * @return bitMap of byte values 138 | */ 139 | function uintsToBitMap(uint256[] memory uints) 140 | internal 141 | pure 142 | returns (uint256 bitMap) 143 | { 144 | /// @solidity memory-safe-assembly 145 | assembly { 146 | // get pointer to first index of array 147 | let uintsIndexPtr := add(uints, 0x20) 148 | // get pointer to first word after final index of array 149 | let finalUintsIndexPtr := add(uintsIndexPtr, shl(5, mload(uints))) 150 | // loop until we reach the end of the array 151 | for { 152 | 153 | } lt(uintsIndexPtr, finalUintsIndexPtr) { 154 | uintsIndexPtr := add(uintsIndexPtr, 0x20) 155 | } { 156 | // set the bit at left-index 'uint' to 1 157 | bitMap := or(bitMap, shl(mload(uintsIndexPtr), 1)) 158 | } 159 | } 160 | } 161 | 162 | /** 163 | * @notice Finds the zero-based index of the first one (right-indexed) in the binary representation of x. 164 | * @param x The uint256 number for which to find the index of the most significant bit. 165 | * @return r The index of the most significant bit as an uint256. 166 | * from: https://gist.github.com/Vectorized/6e5d4271162c931988b385f1fd5a298f 167 | */ 168 | function msb(uint256 x) internal pure returns (uint256 r) { 169 | /// @solidity memory-safe-assembly 170 | assembly { 171 | r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) 172 | r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) 173 | r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) 174 | 175 | x := shr(r, x) 176 | x := or(x, shr(1, x)) 177 | x := or(x, shr(2, x)) 178 | x := or(x, shr(4, x)) 179 | x := or(x, shr(8, x)) 180 | x := or(x, shr(16, x)) 181 | 182 | r := or( 183 | r, 184 | byte( 185 | and(31, shr(27, mul(x, 0x07C4ACDD))), 186 | 0x0009010a0d15021d0b0e10121619031e080c141c0f111807131b17061a05041f 187 | ) 188 | ) 189 | } 190 | } 191 | 192 | /** 193 | * @notice Finds the zero-based index of the first one (left-indexed) in the binary representation of x 194 | * @param x The uint256 number for which to find the index of the least significant bit. 195 | * @return r The index of the least significant bit as an uint256. 196 | * from: // from https://gist.github.com/Atarpara/d6d3773d0ce8958b95804fd36981825f 197 | 198 | */ 199 | function lsb(uint256 x) internal pure returns (uint256 r) { 200 | /// @solidity memory-safe-assembly 201 | assembly { 202 | x := and(x, add(not(x), 1)) 203 | r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) 204 | r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) 205 | r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) 206 | 207 | x := shr(r, x) 208 | x := or(x, shr(1, x)) 209 | x := or(x, shr(2, x)) 210 | x := or(x, shr(4, x)) 211 | x := or(x, shr(8, x)) 212 | x := or(x, shr(16, x)) 213 | 214 | r := or( 215 | r, 216 | byte( 217 | and(31, shr(27, mul(x, 0x07C4ACDD))), 218 | 0x0009010a0d15021d0b0e10121619031e080c141c0f111807131b17061a05041f 219 | ) 220 | ) 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/lib/JSON.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | library json { 5 | /** 6 | * @notice enclose a string in {braces} 7 | * @param value string to enclose in braces 8 | * @return string of {value} 9 | */ 10 | function object(string memory value) internal pure returns (string memory) { 11 | return string.concat('{', value, '}'); 12 | } 13 | 14 | /** 15 | * @notice enclose a string in [brackets] 16 | * @param value string to enclose in brackets 17 | * @return string of [value] 18 | */ 19 | function array(string memory value) internal pure returns (string memory) { 20 | return string.concat('[', value, ']'); 21 | } 22 | 23 | /** 24 | * @notice enclose name and value with quotes, and place a colon "between":"them" 25 | * @param name name of property 26 | * @param value value of property 27 | * @return string of "name":"value" 28 | */ 29 | function property(string memory name, string memory value) 30 | internal 31 | pure 32 | returns (string memory) 33 | { 34 | return string.concat('"', name, '":"', value, '"'); 35 | } 36 | 37 | /** 38 | * @notice enclose name with quotes, but not rawValue, and place a colon "between":them 39 | * @param name name of property 40 | * @param rawValue raw value of property, which will not be enclosed in quotes 41 | * @return string of "name":value 42 | */ 43 | function rawProperty(string memory name, string memory rawValue) 44 | internal 45 | pure 46 | returns (string memory) 47 | { 48 | return string.concat('"', name, '":', rawValue); 49 | } 50 | 51 | /** 52 | * @notice comma-join an array of properties and {"enclose":"them","in":"braces"} 53 | * @param properties array of properties to join 54 | * @return string of {"name":"value","name":"value",...} 55 | */ 56 | function objectOf(string[] memory properties) 57 | internal 58 | pure 59 | returns (string memory) 60 | { 61 | if (properties.length == 0) { 62 | return object(''); 63 | } 64 | string memory result = properties[0]; 65 | for (uint256 i = 1; i < properties.length; ++i) { 66 | result = string.concat(result, ',', properties[i]); 67 | } 68 | return object(result); 69 | } 70 | 71 | /** 72 | * @notice comma-join an array of values and enclose them [in,brackets] 73 | * @param values array of values to join 74 | * @return string of [value,value,...] 75 | */ 76 | function arrayOf(string[] memory values) 77 | internal 78 | pure 79 | returns (string memory) 80 | { 81 | return array(_commaJoin(values)); 82 | } 83 | 84 | /** 85 | * @notice comma-join two arrays of values and [enclose,them,in,brackets] 86 | * @param values1 first array of values to join 87 | * @param values2 second array of values to join 88 | * @return string of [values1_0,values1_1,values2_0,values2_1...] 89 | */ 90 | function arrayOf(string[] memory values1, string[] memory values2) 91 | internal 92 | pure 93 | returns (string memory) 94 | { 95 | if (values1.length == 0) { 96 | return arrayOf(values2); 97 | } else if (values2.length == 0) { 98 | return arrayOf(values1); 99 | } 100 | return 101 | array(string.concat(_commaJoin(values1), ',', _commaJoin(values2))); 102 | } 103 | 104 | /** 105 | * @notice enclose a string in double "quotes" 106 | * @param str string to enclose in quotes 107 | * @return string of "value" 108 | */ 109 | function quote(string memory str) internal pure returns (string memory) { 110 | return string.concat('"', str, '"'); 111 | } 112 | 113 | /** 114 | * @notice comma-join an array of strings 115 | * @param values array of strings to join 116 | * @return string of value,value,... 117 | */ 118 | function _commaJoin(string[] memory values) 119 | internal 120 | pure 121 | returns (string memory) 122 | { 123 | return _join(values, ','); 124 | } 125 | 126 | /** 127 | * @notice join two strings with a comma 128 | * @param value1 first string 129 | * @param value2 second string 130 | * @return string of value1,value2 131 | */ 132 | function _commaJoin(string memory value1, string memory value2) 133 | internal 134 | pure 135 | returns (string memory) 136 | { 137 | return string.concat(value1, ',', value2); 138 | } 139 | 140 | /** 141 | * @notice join an array of strings with a specified separator 142 | * @param values array of strings to join 143 | * @param separator separator to join with 144 | * @return string of valuevalue... 145 | */ 146 | function _join(string[] memory values, string memory separator) 147 | internal 148 | pure 149 | returns (string memory) 150 | { 151 | if (values.length == 0) { 152 | return ''; 153 | } 154 | string memory result = values[0]; 155 | for (uint256 i = 1; i < values.length; ++i) { 156 | result = string.concat(result, separator, values[i]); 157 | } 158 | return result; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/lib/PackedByteUtility.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import '../interface/Constants.sol'; 5 | 6 | library PackedByteUtility { 7 | /** 8 | * @notice get the byte value of a right-indexed byte within a uint256 9 | * @param index right-indexed location of byte within uint256 10 | * @param packedBytes uint256 of bytes 11 | * @return result the byte at right-indexed index within packedBytes 12 | */ 13 | function getPackedByteFromRight(uint256 packedBytes, uint256 index) 14 | internal 15 | pure 16 | returns (uint256 result) 17 | { 18 | /// @solidity memory-safe-assembly 19 | assembly { 20 | result := byte(sub(31, index), packedBytes) 21 | } 22 | } 23 | 24 | /** 25 | * @notice get the byte value of a left-indexed byte within a uint256 26 | * @param index left-indexed location of byte within uint256 27 | * @param packedBytes uint256 of bytes 28 | * @return result the byte at left-indexed index within packedBytes 29 | */ 30 | function getPackedByteFromLeft(uint256 packedBytes, uint256 index) 31 | internal 32 | pure 33 | returns (uint256 result) 34 | { 35 | /// @solidity memory-safe-assembly 36 | assembly { 37 | result := byte(index, packedBytes) 38 | } 39 | } 40 | 41 | function packShortAtIndex( 42 | uint256 packedShorts, 43 | uint256 shortToPack, 44 | uint256 index 45 | ) internal pure returns (uint256 result) { 46 | /// @solidity memory-safe-assembly 47 | assembly { 48 | let shortOffset := sub(240, shl(4, index)) 49 | let mask := xor(MAX_INT, shl(shortOffset, 0xffff)) 50 | result := and(packedShorts, mask) 51 | result := or(result, shl(shortOffset, shortToPack)) 52 | } 53 | } 54 | 55 | function getPackedShortFromRight(uint256 packed, uint256 index) 56 | internal 57 | pure 58 | returns (uint256 result) 59 | { 60 | assembly { 61 | let shortOffset := shl(4, index) 62 | result := shr(shortOffset, packed) 63 | result := and(result, 0xffff) 64 | } 65 | } 66 | 67 | function getPackedNFromRight( 68 | uint256 packed, 69 | uint256 bitsPerIndex, 70 | uint256 index 71 | ) internal pure returns (uint256 result) { 72 | assembly { 73 | let offset := mul(bitsPerIndex, index) 74 | let mask := sub(shl(bitsPerIndex, 1), 1) 75 | result := shr(offset, packed) 76 | result := and(result, mask) 77 | } 78 | } 79 | 80 | function packNAtRightIndex( 81 | uint256 packed, 82 | uint256 bitsPerIndex, 83 | uint256 toPack, 84 | uint256 index 85 | ) internal pure returns (uint256 result) { 86 | assembly { 87 | // left-shift offset 88 | let offset := mul(bitsPerIndex, index) 89 | // mask for 2**n uint 90 | let nMask := sub(shl(bitsPerIndex, 1), 1) 91 | // mask to clear bits at offset 92 | let mask := xor(MAX_INT, shl(offset, nMask)) 93 | // clear bits at offset 94 | result := and(packed, mask) 95 | // shift toPack to offset, then pack 96 | result := or(result, shl(offset, toPack)) 97 | } 98 | } 99 | 100 | function getPackedShortFromLeft(uint256 packed, uint256 index) 101 | internal 102 | pure 103 | returns (uint256 result) 104 | { 105 | assembly { 106 | let shortOffset := sub(240, shl(4, index)) 107 | result := shr(shortOffset, packed) 108 | result := and(result, 0xffff) 109 | } 110 | } 111 | 112 | /** 113 | * @notice unpack elements of a packed byte array into a bitmap. Short-circuits at first 0-byte. 114 | * @param packedBytes uint256 of bytes 115 | * @return unpacked - 1-indexed bitMap of all byte values contained in packedBytes up until the first 0-byte 116 | */ 117 | function unpackBytesToBitMap(uint256 packedBytes) 118 | internal 119 | pure 120 | returns (uint256 unpacked) 121 | { 122 | /// @solidity memory-safe-assembly 123 | assembly { 124 | for { 125 | let i := 0 126 | } lt(i, 32) { 127 | i := add(i, 1) 128 | } { 129 | // this is the ID of the layer, eg, 1, 5, 253 130 | let byteVal := byte(i, packedBytes) 131 | // don't count zero bytes 132 | if iszero(byteVal) { 133 | break 134 | } 135 | // byteVals are 1-indexed because we're shifting 1 by the value of the byte 136 | unpacked := or(unpacked, shl(byteVal, 1)) 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * @notice pack byte values into a uint256. Note: *will not* short-circuit on first 0-byte 143 | * @param arrayOfBytes uint256[] of byte values 144 | * @return packed uint256 of packed bytes 145 | */ 146 | function packArrayOfBytes(uint256[] memory arrayOfBytes) 147 | internal 148 | pure 149 | returns (uint256 packed) 150 | { 151 | /// @solidity memory-safe-assembly 152 | assembly { 153 | let arrayOfBytesIndexPtr := add(arrayOfBytes, 0x20) 154 | let arrayOfBytesLength := mload(arrayOfBytes) 155 | if gt(arrayOfBytesLength, 32) { 156 | arrayOfBytesLength := 32 157 | } 158 | let finalI := shl(3, arrayOfBytesLength) 159 | let i 160 | for { 161 | 162 | } lt(i, finalI) { 163 | arrayOfBytesIndexPtr := add(0x20, arrayOfBytesIndexPtr) 164 | i := add(8, i) 165 | } { 166 | packed := or( 167 | packed, 168 | shl(sub(248, i), mload(arrayOfBytesIndexPtr)) 169 | ) 170 | } 171 | } 172 | } 173 | 174 | function packArrayOfShorts(uint256[] memory shorts) 175 | internal 176 | pure 177 | returns (uint256[2] memory packed) 178 | { 179 | packed = [uint256(0), uint256(0)]; 180 | for (uint256 i; i < shorts.length; i++) { 181 | if (i == 32) { 182 | break; 183 | } 184 | uint256 j = i / 16; 185 | uint256 index = i % 16; 186 | packed[j] = packShortAtIndex(packed[j], shorts[i], index); 187 | } 188 | } 189 | 190 | /** 191 | * @notice Unpack a packed uint256 of bytes into a uint256 array of byte values. Short-circuits on first 0-byte. 192 | * @param packedByteArray The packed uint256 of bytes to unpack 193 | * @return unpacked uint256[] The unpacked uint256 array of bytes 194 | */ 195 | function unpackByteArray(uint256 packedByteArray) 196 | internal 197 | pure 198 | returns (uint256[] memory unpacked) 199 | { 200 | /// @solidity memory-safe-assembly 201 | assembly { 202 | unpacked := mload(0x40) 203 | let unpackedIndexPtr := add(0x20, unpacked) 204 | let maxUnpackedIndexPtr := add(unpackedIndexPtr, shl(5, 32)) 205 | let numBytes 206 | for { 207 | 208 | } lt(unpackedIndexPtr, maxUnpackedIndexPtr) { 209 | unpackedIndexPtr := add(0x20, unpackedIndexPtr) 210 | numBytes := add(1, numBytes) 211 | } { 212 | let byteVal := byte(numBytes, packedByteArray) 213 | if iszero(byteVal) { 214 | break 215 | } 216 | mstore(unpackedIndexPtr, byteVal) 217 | } 218 | // store the number of layers at the pointer to unpacked array 219 | mstore(unpacked, numBytes) 220 | // update free mem pointer to be old mem ptr + 0x20 (32-byte array length) + 0x20 * numLayers (each 32-byte element) 221 | mstore(0x40, add(unpacked, add(0x20, shl(5, numBytes)))) 222 | } 223 | } 224 | 225 | /** 226 | * @notice given a uint256 packed array of bytes, pack a byte at an index from the left 227 | * @param packedBytes existing packed bytes 228 | * @param byteToPack byte to pack into packedBytes 229 | * @param index index to pack byte at 230 | * @return newPackedBytes with byteToPack at index 231 | */ 232 | function packByteAtIndex( 233 | uint256 packedBytes, 234 | uint256 byteToPack, 235 | uint256 index 236 | ) internal pure returns (uint256 newPackedBytes) { 237 | /// @solidity memory-safe-assembly 238 | assembly { 239 | // calculate left-indexed bit offset of byte within packedBytes 240 | let byteOffset := sub(248, shl(3, index)) 241 | // create a mask to clear the bits we're about to overwrite 242 | let mask := xor(MAX_INT, shl(byteOffset, 0xff)) 243 | // copy packedBytes to newPackedBytes, clearing the relevant bits 244 | newPackedBytes := and(packedBytes, mask) 245 | // shift the byte to the offset and OR it into newPackedBytes 246 | newPackedBytes := or(newPackedBytes, shl(byteOffset, byteToPack)) 247 | } 248 | } 249 | 250 | /// @dev less efficient logic for packing >32 bytes into >1 uint256 251 | function packArraysOfBytes(uint256[] memory arrayOfBytes) 252 | internal 253 | pure 254 | returns (uint256[] memory) 255 | { 256 | uint256 arrayOfBytesLength = arrayOfBytes.length; 257 | uint256[] memory packed = new uint256[]( 258 | (arrayOfBytesLength - 1) / 32 + 1 259 | ); 260 | uint256 workingWord = 0; 261 | for (uint256 i = 0; i < arrayOfBytesLength; ) { 262 | // OR workingWord with this byte shifted by byte within the word 263 | workingWord |= uint256(arrayOfBytes[i]) << (8 * (31 - (i % 32))); 264 | 265 | // if we're on the last byte of the word, store in array 266 | if (i % 32 == 31) { 267 | uint256 j = i / 32; 268 | packed[j] = workingWord; 269 | workingWord = 0; 270 | } 271 | unchecked { 272 | ++i; 273 | } 274 | } 275 | if (arrayOfBytesLength % 32 != 0) { 276 | packed[packed.length - 1] = workingWord; 277 | } 278 | 279 | return packed; 280 | } 281 | 282 | /// @dev less efficient logic for unpacking >1 uint256s into >32 byte values 283 | function unpackByteArrays(uint256[] memory packedByteArrays) 284 | internal 285 | pure 286 | returns (uint256[] memory) 287 | { 288 | uint256 packedByteArraysLength = packedByteArrays.length; 289 | uint256[] memory unpacked = new uint256[](packedByteArraysLength * 32); 290 | for (uint256 i = 0; i < packedByteArraysLength; ) { 291 | uint256 packedByteArray = packedByteArrays[i]; 292 | uint256 j = 0; 293 | for (; j < 32; ) { 294 | uint256 unpackedByte = getPackedByteFromLeft( 295 | j, 296 | packedByteArray 297 | ); 298 | if (unpackedByte == 0) { 299 | break; 300 | } 301 | unpacked[i * 32 + j] = unpackedByte; 302 | unchecked { 303 | ++j; 304 | } 305 | } 306 | if (j < 32) { 307 | break; 308 | } 309 | unchecked { 310 | ++i; 311 | } 312 | } 313 | return unpacked; 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/metadata/IImageLayerable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | interface IImageLayerable { 5 | function setBaseLayerURI(string calldata baseLayerURI) external; 6 | 7 | function setDefaultURI(string calldata baseLayerURI) external; 8 | 9 | function getDefaultImageURI(uint256 layerId) 10 | external 11 | returns (string memory); 12 | } 13 | -------------------------------------------------------------------------------- /src/metadata/ILayerable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | interface ILayerable { 5 | function getLayerImageURI(uint256 layerId) 6 | external 7 | view 8 | returns (string memory); 9 | 10 | function getLayeredTokenImageURI(uint256[] calldata activeLayers) 11 | external 12 | view 13 | returns (string memory); 14 | 15 | function getBoundLayerTraits(uint256 bindings) 16 | external 17 | view 18 | returns (string memory); 19 | 20 | function getActiveLayerTraits(uint256[] calldata activeLayers) 21 | external 22 | view 23 | returns (string memory); 24 | 25 | function getBoundAndActiveLayerTraits( 26 | uint256 bindings, 27 | uint256[] calldata activeLayers 28 | ) external view returns (string memory); 29 | 30 | function getTokenURI( 31 | uint256 tokenId, 32 | uint256 layerId, 33 | uint256 bindings, 34 | uint256[] calldata activeLayers, 35 | bytes32 layerSeed 36 | ) external view returns (string memory); 37 | } 38 | -------------------------------------------------------------------------------- /src/metadata/ImageLayerable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {OnChainTraits} from '../traits/OnChainTraits.sol'; 5 | import {svg} from '../SVG.sol'; 6 | import {json} from '../lib/JSON.sol'; 7 | import {Layerable} from './Layerable.sol'; 8 | import {IImageLayerable} from './IImageLayerable.sol'; 9 | import {InvalidInitialization} from '../interface/Errors.sol'; 10 | import {Attribute} from '../interface/Structs.sol'; 11 | import {DisplayType} from '../interface/Enums.sol'; 12 | import {Base64} from 'solady/utils/Base64.sol'; 13 | import {LibString} from 'solady/utils/LibString.sol'; 14 | 15 | contract ImageLayerable is Layerable, IImageLayerable { 16 | // TODO: different strings impl? 17 | using LibString for uint256; 18 | 19 | string defaultURI; 20 | string baseLayerURI; 21 | 22 | uint256 width; 23 | uint256 height; 24 | 25 | string externalUrl; 26 | string description; 27 | 28 | // TODO: add baseLayerURI 29 | constructor( 30 | address _owner, 31 | string memory _defaultURI, 32 | uint256 _width, 33 | uint256 _height, 34 | string memory _externalUrl, 35 | string memory _description 36 | ) Layerable(_owner) { 37 | _initialize(_defaultURI, _width, _height, _externalUrl, _description); 38 | } 39 | 40 | function initialize( 41 | address _owner, 42 | string memory _defaultURI, 43 | uint256 _width, 44 | uint256 _height, 45 | string memory _externalUrl, 46 | string memory _description 47 | ) public virtual { 48 | super._initialize(_owner); 49 | _initialize(_defaultURI, _width, _height, _externalUrl, _description); 50 | } 51 | 52 | function _initialize( 53 | string memory _defaultURI, 54 | uint256 _width, 55 | uint256 _height, 56 | string memory _externalUrl, 57 | string memory _description 58 | ) internal virtual { 59 | if (address(this).code.length > 0) { 60 | revert InvalidInitialization(); 61 | } 62 | defaultURI = _defaultURI; 63 | width = _width; 64 | height = _height; 65 | externalUrl = _externalUrl; 66 | description = _description; 67 | } 68 | 69 | function setWidth(uint256 _width) external onlyOwner { 70 | width = _width; 71 | } 72 | 73 | function setHeight(uint256 _height) external onlyOwner { 74 | height = _height; 75 | } 76 | 77 | /// @notice set the default URI for unrevealed tokens 78 | function setDefaultURI(string memory _defaultURI) public onlyOwner { 79 | defaultURI = _defaultURI; 80 | } 81 | 82 | /// @notice set the base URI for layers 83 | function setBaseLayerURI(string memory _baseLayerURI) public onlyOwner { 84 | baseLayerURI = _baseLayerURI; 85 | } 86 | 87 | /// @notice set the external URL for all tokens 88 | function setExternalUrl(string memory _externalUrl) public onlyOwner { 89 | externalUrl = _externalUrl; 90 | } 91 | 92 | /// @notice set the description for all tokens 93 | function setDescription(string memory _description) public onlyOwner { 94 | description = _description; 95 | } 96 | 97 | /** 98 | * @notice get the raw URI of a set of token traits, not encoded as a data uri 99 | * @param layerId the layerId of the base token 100 | * @param bindings the bitmap of bound traits 101 | * @param activeLayers packed array of active layerIds as bytes 102 | * @param layerSeed the random seed for random generation of traits, used to determine if layers have been revealed 103 | * @return the complete URI of the token, including image and all attributes 104 | */ 105 | function _getRawTokenJson( 106 | uint256 tokenId, 107 | uint256 layerId, 108 | uint256 bindings, 109 | uint256[] calldata activeLayers, 110 | bytes32 layerSeed 111 | ) internal view virtual override returns (string memory) { 112 | string memory name = _getName(tokenId, layerId, bindings); 113 | string memory _externalUrl = _getExternalUrl(tokenId, layerId); 114 | string memory _description = _getDescription(tokenId, layerId); 115 | // return default uri 116 | if (layerSeed == 0) { 117 | return 118 | _constructJson( 119 | name, 120 | _externalUrl, 121 | _description, 122 | getDefaultImageURI(layerId), 123 | '' 124 | ); 125 | } 126 | // if no bindings, format metadata as an individual NFT 127 | // check if bindings == 0 or 1; bindable layers will be treated differently 128 | else if (bindings == 0 || bindings == 1) { 129 | return _getRawLayerJson(name, _externalUrl, _description, layerId); 130 | } else { 131 | return 132 | _constructJson( 133 | name, 134 | _externalUrl, 135 | _description, 136 | getLayeredTokenImageURI(activeLayers), 137 | getBoundAndActiveLayerTraits(bindings, activeLayers) 138 | ); 139 | } 140 | } 141 | 142 | function _getRawLayerJson( 143 | string memory name, 144 | string memory _externalUrl, 145 | string memory _description, 146 | uint256 layerId 147 | ) internal view virtual override returns (string memory) { 148 | Attribute memory layerTypeAttribute = traitAttributes[layerId]; 149 | layerTypeAttribute.value = layerTypeAttribute.traitType; 150 | layerTypeAttribute.traitType = 'Layer Type'; 151 | layerTypeAttribute.displayType = DisplayType.String; 152 | return 153 | _constructJson( 154 | name, 155 | _externalUrl, 156 | _description, 157 | getLayerImageURI(layerId), 158 | json.array( 159 | json._commaJoin( 160 | _getAttributeJson(layerTypeAttribute), 161 | getLayerTraitJson(layerId) 162 | ) 163 | ) 164 | ); 165 | } 166 | 167 | function _getName( 168 | uint256 tokenId, 169 | uint256, 170 | uint256 171 | ) internal view virtual override returns (string memory) { 172 | return tokenId.toString(); 173 | } 174 | 175 | function _getExternalUrl(uint256, uint256) 176 | internal 177 | view 178 | virtual 179 | override 180 | returns (string memory) 181 | { 182 | return externalUrl; 183 | } 184 | 185 | function _getDescription(uint256, uint256) 186 | internal 187 | view 188 | virtual 189 | override 190 | returns (string memory) 191 | { 192 | return description; 193 | } 194 | 195 | /// @notice get the complete SVG for a set of activeLayers 196 | function getLayeredTokenImageURI(uint256[] calldata activeLayers) 197 | public 198 | view 199 | virtual 200 | override 201 | returns (string memory) 202 | { 203 | string memory layerImages = ''; 204 | for (uint256 i; i < activeLayers.length; ++i) { 205 | string memory layerUri = getLayerImageURI(activeLayers[i]); 206 | layerImages = string.concat( 207 | layerImages, 208 | svg.image( 209 | layerUri, 210 | string.concat( 211 | svg.prop('height', '100%'), 212 | ' ', 213 | svg.prop('width', '100%') 214 | ) 215 | ) 216 | ); 217 | } 218 | 219 | return 220 | string.concat( 221 | 'data:image/svg+xml;base64,', 222 | Base64.encode( 223 | bytes( 224 | string.concat( 225 | '', 230 | layerImages, 231 | '' 232 | ) 233 | ) 234 | ) 235 | ); 236 | } 237 | 238 | /// @notice get the image URI for a layerId 239 | function getLayerImageURI(uint256 layerId) 240 | public 241 | view 242 | virtual 243 | override 244 | returns (string memory) 245 | { 246 | return string.concat(baseLayerURI, layerId.toString()); 247 | } 248 | 249 | /// @notice get the default URI for a layerId 250 | function getDefaultImageURI(uint256) 251 | public 252 | view 253 | virtual 254 | override 255 | returns (string memory) 256 | { 257 | return defaultURI; 258 | } 259 | 260 | /// @dev helper to wrap imageURI and optional attributes into a JSON object string 261 | function _constructJson( 262 | string memory name, 263 | string memory _externalUrl, 264 | string memory _description, 265 | string memory imageURI, 266 | string memory attributes 267 | ) internal pure returns (string memory) { 268 | string[] memory properties; 269 | string memory nameProperty = json.property('name', name); 270 | string memory externalUrlProperty = json.property( 271 | 'external_url', 272 | _externalUrl 273 | ); 274 | string memory descriptionProperty = json.property( 275 | 'description', 276 | _description 277 | ); 278 | if (bytes(attributes).length > 0) { 279 | properties = new string[](5); 280 | properties[0] = nameProperty; 281 | properties[1] = externalUrlProperty; 282 | properties[2] = descriptionProperty; 283 | properties[3] = json.property('image', imageURI); 284 | // attributes should be a JSON array, no need to wrap it in quotes 285 | properties[4] = json.rawProperty('attributes', attributes); 286 | } else { 287 | properties = new string[](4); 288 | properties[0] = nameProperty; 289 | properties[1] = externalUrlProperty; 290 | properties[2] = descriptionProperty; 291 | properties[3] = json.property('image', imageURI); 292 | } 293 | return json.objectOf(properties); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/metadata/Layerable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {OnChainTraits} from '../traits/OnChainTraits.sol'; 5 | import {svg, utils} from '../SVG.sol'; 6 | import {RandomTraits} from '../traits/RandomTraits.sol'; 7 | import {json} from '../lib/JSON.sol'; 8 | import {BitMapUtility} from '../lib/BitMapUtility.sol'; 9 | import {PackedByteUtility} from '../lib/PackedByteUtility.sol'; 10 | import {ILayerable} from './ILayerable.sol'; 11 | import {InvalidInitialization} from '../interface/Errors.sol'; 12 | 13 | import {Base64} from 'solady/utils/Base64.sol'; 14 | import {LibString} from 'solady/utils/LibString.sol'; 15 | import {Attribute} from '../interface/Structs.sol'; 16 | import {DisplayType} from '../interface/Enums.sol'; 17 | 18 | abstract contract Layerable is ILayerable, OnChainTraits { 19 | using BitMapUtility for uint256; 20 | using LibString for uint256; 21 | 22 | constructor(address _owner) { 23 | _initialize(_owner); 24 | } 25 | 26 | function initialize(address _owner) external virtual { 27 | _initialize(_owner); 28 | } 29 | 30 | function _initialize(address _owner) internal virtual { 31 | if (address(this).code.length > 0) { 32 | revert InvalidInitialization(); 33 | } 34 | _transferOwnership(_owner); 35 | } 36 | 37 | /** 38 | * @notice get the complete URI of a set of token traits, encoded as a data-uri 39 | * @param layerId the layerId of the base token 40 | * @param bindings the bitmap of bound traits 41 | * @param activeLayers packed array of active layerIds as bytes 42 | * @param layerSeed the random seed for random generation of traits, used to determine if layers have been revealed 43 | * @return the complete data URI of the token, including image and all attributes 44 | */ 45 | function getTokenURI( 46 | uint256 tokenId, 47 | uint256 layerId, 48 | uint256 bindings, 49 | uint256[] calldata activeLayers, 50 | bytes32 layerSeed 51 | ) public view virtual returns (string memory) { 52 | // TODO: maybe don't base64 encode? just base64 encode svg? 53 | return 54 | string.concat( 55 | 'data:application/json;base64,', 56 | Base64.encode( 57 | bytes( 58 | _getRawTokenJson( 59 | tokenId, 60 | layerId, 61 | bindings, 62 | activeLayers, 63 | layerSeed 64 | ) 65 | ) 66 | ) 67 | ); 68 | } 69 | 70 | function getTokenJson( 71 | uint256 tokenId, 72 | uint256 layerId, 73 | uint256 bindings, 74 | uint256[] calldata activeLayers, 75 | bytes32 layerSeed 76 | ) public view virtual returns (string memory) { 77 | return 78 | _getRawTokenJson( 79 | tokenId, 80 | layerId, 81 | bindings, 82 | activeLayers, 83 | layerSeed 84 | ); 85 | } 86 | 87 | function getLayerJson(uint256 layerId) 88 | public 89 | view 90 | virtual 91 | returns (string memory) 92 | { 93 | return 94 | _getRawLayerJson( 95 | _getName(layerId, layerId, 0), 96 | _getExternalUrl(layerId, layerId), 97 | _getDescription(layerId, layerId), 98 | layerId 99 | ); 100 | } 101 | 102 | function _getRawTokenJson( 103 | uint256 tokenId, 104 | uint256 layerId, 105 | uint256 bindings, 106 | uint256[] calldata activeLayers, 107 | bytes32 layerSeed 108 | ) internal view virtual returns (string memory); 109 | 110 | function _getRawLayerJson( 111 | string memory name, 112 | string memory _externalUrl, 113 | string memory description, 114 | uint256 layerId 115 | ) internal view virtual returns (string memory); 116 | 117 | function _getName( 118 | uint256 tokenId, 119 | uint256 layerId, 120 | uint256 bindings 121 | ) internal view virtual returns (string memory); 122 | 123 | function _getExternalUrl(uint256 tokenId, uint256 layerId) 124 | internal 125 | view 126 | virtual 127 | returns (string memory); 128 | 129 | function _getDescription(uint256 tokenId, uint256 layerId) 130 | internal 131 | view 132 | virtual 133 | returns (string memory); 134 | 135 | /// @notice get the complete SVG for a set of activeLayers 136 | function getLayeredTokenImageURI(uint256[] calldata activeLayers) 137 | public 138 | view 139 | virtual 140 | returns (string memory); 141 | 142 | /// @notice get the image URI for a layerId 143 | function getLayerImageURI(uint256 layerId) 144 | public 145 | view 146 | virtual 147 | returns (string memory); 148 | 149 | /// @notice get stringified JSON array of bound layer traits 150 | function getBoundLayerTraits(uint256 bindings) 151 | public 152 | view 153 | returns (string memory) 154 | { 155 | return json.arrayOf(_getBoundLayerTraits(bindings & ~uint256(0))); 156 | } 157 | 158 | /// @notice get stringified JSON array of active layer traits 159 | function getActiveLayerTraits(uint256[] calldata activeLayers) 160 | public 161 | view 162 | returns (string memory) 163 | { 164 | return json.arrayOf(_getActiveLayerTraits(activeLayers)); 165 | } 166 | 167 | /// @notice get stringified JSON array of combined bound and active layer traits 168 | function getBoundAndActiveLayerTraits( 169 | uint256 bindings, 170 | uint256[] calldata activeLayers 171 | ) public view returns (string memory) { 172 | string[] memory layerTraits = _getBoundLayerTraits(bindings); 173 | string[] memory activeLayerTraits = _getActiveLayerTraits(activeLayers); 174 | return json.arrayOf(layerTraits, activeLayerTraits); 175 | } 176 | 177 | /// @dev get array of stringified trait json for bindings 178 | function _getBoundLayerTraits(uint256 bindings) 179 | internal 180 | view 181 | returns (string[] memory layerTraits) 182 | { 183 | uint256[] memory boundLayers = BitMapUtility.unpackBitMap(bindings); 184 | 185 | layerTraits = new string[](boundLayers.length + 1); 186 | for (uint256 i; i < boundLayers.length; ++i) { 187 | layerTraits[i] = getLayerTraitJson(boundLayers[i]); 188 | } 189 | Attribute memory layerCount = Attribute( 190 | 'Layer Count', 191 | boundLayers.length.toString(), 192 | DisplayType.Number 193 | ); 194 | 195 | layerTraits[boundLayers.length] = _getAttributeJson(layerCount); 196 | } 197 | 198 | /// @dev get array of stringified trait json for active layers. Prepends "Active" to trait title. 199 | // eg 'Background' -> 'Active Background' 200 | function _getActiveLayerTraits(uint256[] calldata activeLayers) 201 | internal 202 | view 203 | returns (string[] memory activeLayerTraits) 204 | { 205 | activeLayerTraits = new string[](activeLayers.length); 206 | for (uint256 i; i < activeLayers.length; ++i) { 207 | activeLayerTraits[i] = getLayerTraitJson(activeLayers[i], 'Active'); 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/traits/OnChainMultiTraits.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {TwoStepOwnable} from 'utility-contracts/TwoStepOwnable.sol'; 5 | import {PackedByteUtility} from '../lib/PackedByteUtility.sol'; 6 | import {LibString} from 'solady/utils/LibString.sol'; 7 | import {json} from '../lib/JSON.sol'; 8 | import {ArrayLengthMismatch} from '../interface/Errors.sol'; 9 | import {DisplayType} from '../interface/Enums.sol'; 10 | import {Attribute} from '../interface/Structs.sol'; 11 | 12 | abstract contract OnChainMultiTraits is TwoStepOwnable { 13 | using LibString for uint256; 14 | 15 | mapping(uint256 => Attribute[]) public traitAttributes; 16 | 17 | function setAttribute(uint256 layerId, Attribute[] calldata attribute) 18 | public 19 | onlyOwner 20 | { 21 | _setAttribute(layerId, attribute); 22 | } 23 | 24 | function setAttributes( 25 | uint256[] calldata layerIds, 26 | Attribute[][] calldata attributes 27 | ) public onlyOwner { 28 | if (layerIds.length != attributes.length) { 29 | revert ArrayLengthMismatch(layerIds.length, attributes.length); 30 | } 31 | for (uint256 i; i < layerIds.length; ++i) { 32 | _setAttribute(layerIds[i], attributes[i]); 33 | } 34 | } 35 | 36 | function _setAttribute(uint256 layerId, Attribute[] calldata attribute) 37 | internal 38 | { 39 | delete traitAttributes[layerId]; 40 | Attribute[] storage storedAttributes = traitAttributes[layerId]; 41 | uint256 attributesLength = attribute.length; 42 | for (uint256 i = 0; i < attributesLength; ++i) { 43 | storedAttributes.push(attribute[i]); 44 | } 45 | } 46 | 47 | function getLayerTraitJson(uint256 layerId) 48 | public 49 | view 50 | returns (string memory) 51 | { 52 | Attribute[] memory attributes = traitAttributes[layerId]; 53 | uint256 attributesLength = attributes.length; 54 | string[] memory attributeJsons = new string[](attributesLength); 55 | for (uint256 i; i < attributesLength; ++i) { 56 | attributeJsons[i] = getAttributeJson(attributes[i]); 57 | } 58 | if (attributesLength == 1) { 59 | return attributeJsons[0]; 60 | } 61 | return json._commaJoin(attributeJsons); 62 | } 63 | 64 | function getLayerTraitJson(uint256 layerId, string memory qualifier) 65 | public 66 | view 67 | returns (string memory) 68 | { 69 | Attribute[] memory attributes = traitAttributes[layerId]; 70 | uint256 attributesLength = attributes.length; 71 | string[] memory attributeJsons = new string[](attributesLength); 72 | for (uint256 i; i < attributesLength; ++i) { 73 | attributeJsons[i] = getAttributeJson(attributes[i], qualifier); 74 | } 75 | return json._commaJoin(attributeJsons); 76 | } 77 | 78 | function getAttributeJson(Attribute memory attribute) 79 | public 80 | pure 81 | returns (string memory) 82 | { 83 | string memory properties = string.concat( 84 | json.property('trait_type', attribute.traitType), 85 | ',' 86 | ); 87 | return _getAttributeJson(properties, attribute); 88 | } 89 | 90 | function getAttributeJson( 91 | Attribute memory attribute, 92 | string memory qualifier 93 | ) public pure returns (string memory) { 94 | string memory properties = string.concat( 95 | json.property( 96 | 'trait_type', 97 | string.concat(qualifier, ' ', attribute.traitType) 98 | ), 99 | ',' 100 | ); 101 | return _getAttributeJson(properties, attribute); 102 | } 103 | 104 | function displayTypeJson(string memory displayTypeString) 105 | internal 106 | pure 107 | returns (string memory) 108 | { 109 | return json.property('display_type', displayTypeString); 110 | } 111 | 112 | function _getAttributeJson( 113 | string memory properties, 114 | Attribute memory attribute 115 | ) internal pure returns (string memory) { 116 | // todo: probably don't need this for layers, but good for generic 117 | DisplayType displayType = attribute.displayType; 118 | if (displayType != DisplayType.String) { 119 | string memory displayTypeString; 120 | if (displayType == DisplayType.Number) { 121 | displayTypeString = displayTypeJson('number'); 122 | } else if (attribute.displayType == DisplayType.Date) { 123 | displayTypeString = displayTypeJson('date'); 124 | } else if (attribute.displayType == DisplayType.BoostPercent) { 125 | displayTypeString = displayTypeJson('boost_percent'); 126 | } else if (attribute.displayType == DisplayType.BoostNumber) { 127 | displayTypeString = displayTypeJson('boost_number'); 128 | } 129 | properties = string.concat(properties, displayTypeString, ','); 130 | } 131 | properties = string.concat( 132 | properties, 133 | json.property('value', attribute.value) 134 | ); 135 | return json.object(properties); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/traits/OnChainTraits.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {TwoStepOwnable} from 'utility-contracts/TwoStepOwnable.sol'; 5 | import {json} from '../lib/JSON.sol'; 6 | import {ArrayLengthMismatch} from '../interface/Errors.sol'; 7 | import {DisplayType} from '../interface/Enums.sol'; 8 | import {Attribute} from '../interface/Structs.sol'; 9 | 10 | abstract contract OnChainTraits is TwoStepOwnable { 11 | mapping(uint256 => Attribute) public traitAttributes; 12 | 13 | function setAttribute(uint256 traitId, Attribute calldata attribute) 14 | public 15 | onlyOwner 16 | { 17 | traitAttributes[traitId] = attribute; 18 | } 19 | 20 | function setAttributes( 21 | uint256[] calldata traitIds, 22 | Attribute[] calldata attributes 23 | ) public onlyOwner { 24 | if (traitIds.length != attributes.length) { 25 | revert ArrayLengthMismatch(traitIds.length, attributes.length); 26 | } 27 | for (uint256 i; i < traitIds.length; ++i) { 28 | traitAttributes[traitIds[i]] = attributes[i]; 29 | } 30 | } 31 | 32 | function getLayerTraitJson(uint256 traitId) 33 | public 34 | view 35 | returns (string memory) 36 | { 37 | Attribute memory attribute = traitAttributes[traitId]; 38 | return _getAttributeJson(attribute); 39 | } 40 | 41 | function getLayerTraitJson(uint256 traitId, string memory qualifier) 42 | public 43 | view 44 | returns (string memory) 45 | { 46 | Attribute memory attribute = traitAttributes[traitId]; 47 | return _getAttributeJson(attribute, qualifier); 48 | } 49 | 50 | function _getAttributeJson(Attribute memory attribute) 51 | internal 52 | pure 53 | returns (string memory) 54 | { 55 | string memory properties = string.concat( 56 | json.property('trait_type', attribute.traitType), 57 | ',' 58 | ); 59 | return _getAttributeJson(properties, attribute); 60 | } 61 | 62 | function _getAttributeJson( 63 | Attribute memory attribute, 64 | string memory qualifier 65 | ) internal pure returns (string memory) { 66 | string memory properties = string.concat( 67 | json.property( 68 | 'trait_type', 69 | string.concat(qualifier, ' ', attribute.traitType) 70 | ), 71 | ',' 72 | ); 73 | return _getAttributeJson(properties, attribute); 74 | } 75 | 76 | function displayTypeJson(string memory displayTypeString) 77 | internal 78 | pure 79 | returns (string memory) 80 | { 81 | return json.property('display_type', displayTypeString); 82 | } 83 | 84 | function _getAttributeJson( 85 | string memory properties, 86 | Attribute memory attribute 87 | ) internal pure returns (string memory) { 88 | // todo: probably don't need this for layers, but good for generic 89 | DisplayType displayType = attribute.displayType; 90 | if (displayType != DisplayType.String) { 91 | string memory displayTypeString; 92 | if (displayType == DisplayType.Number) { 93 | displayTypeString = displayTypeJson('number'); 94 | } else if (attribute.displayType == DisplayType.Date) { 95 | displayTypeString = displayTypeJson('date'); 96 | } else if (attribute.displayType == DisplayType.BoostPercent) { 97 | displayTypeString = displayTypeJson('boost_percent'); 98 | } else if (attribute.displayType == DisplayType.BoostNumber) { 99 | displayTypeString = displayTypeJson('boost_number'); 100 | } 101 | properties = string.concat(properties, displayTypeString, ','); 102 | } 103 | properties = string.concat( 104 | properties, 105 | json.property('value', attribute.value) 106 | ); 107 | return json.object(properties); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/traits/RandomTraits.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {BAD_DISTRIBUTIONS_SIGNATURE} from '../interface/Constants.sol'; 5 | import {BadDistributions, InvalidLayerType, ArrayLengthMismatch, BatchNotRevealed} from '../interface/Errors.sol'; 6 | import {BatchVRFConsumer} from '../vrf/BatchVRFConsumer.sol'; 7 | 8 | abstract contract RandomTraits is BatchVRFConsumer { 9 | // 32 possible traits per layerType given uint16 distributions 10 | // except final trait type, which has 31, because 0 is not a valid layerId. 11 | // Function getLayerId will check if layerSeed is less than the distribution, 12 | // so traits distribution cutoffs should be sorted left-to-right 13 | // ie smallest packed 16-bit segment should be the leftmost 16 bits 14 | // TODO: does this mean for N < 32 traits, there should be N-1 distributions? 15 | mapping(uint8 => uint256[2]) layerTypeToPackedDistributions; 16 | 17 | constructor( 18 | string memory name, 19 | string memory symbol, 20 | address vrfCoordinatorAddress, 21 | uint240 maxNumSets, 22 | uint8 numTokensPerSet, 23 | uint64 subscriptionId, 24 | uint8 numRandomBatches, 25 | bytes32 keyHash 26 | ) 27 | BatchVRFConsumer( 28 | name, 29 | symbol, 30 | vrfCoordinatorAddress, 31 | maxNumSets, 32 | numTokensPerSet, 33 | subscriptionId, 34 | numRandomBatches, 35 | keyHash 36 | ) 37 | {} 38 | 39 | ///////////// 40 | // SETTERS // 41 | ///////////// 42 | 43 | /** 44 | * @notice Set the probability distribution for up to 32 different layer traitIds 45 | * @param layerType layer type to set distribution for 46 | * @param distribution a uint256[2] comprised of sorted, packed shorts 47 | * that will be compared against a random short to determine the layerId 48 | * for a given tokenId 49 | */ 50 | function setLayerTypeDistribution( 51 | uint8 layerType, 52 | uint256[2] calldata distribution 53 | ) public virtual onlyOwner { 54 | _setLayerTypeDistribution(layerType, distribution); 55 | } 56 | 57 | /** 58 | * @notice Set layer type distributions for multiple layer types 59 | * @param layerTypes layer types to set distribution for 60 | * @param distributions an array of uint256[2]s comprised of sorted, packed shorts 61 | * that will be compared against a random short to determine the layerId 62 | * for a given tokenId 63 | */ 64 | function setLayerTypeDistributions( 65 | uint8[] calldata layerTypes, 66 | uint256[2][] calldata distributions 67 | ) public virtual onlyOwner { 68 | if (layerTypes.length != distributions.length) { 69 | revert ArrayLengthMismatch(layerTypes.length, distributions.length); 70 | } 71 | for (uint8 i = 0; i < layerTypes.length; i++) { 72 | _setLayerTypeDistribution(layerTypes[i], distributions[i]); 73 | } 74 | } 75 | 76 | /** 77 | * @notice calculate the 16-bit seed for a layer by hashing the packedBatchRandomness, tokenId, and layerType together 78 | * and truncating to 16 bits 79 | * @param tokenId tokenId to get seed for 80 | * @param layerType layer type to get seed for 81 | * @param seed packedBatchRandomness 82 | * @return layerSeed - 16-bit seed for the given tokenId and layerType 83 | */ 84 | function getLayerSeed( 85 | uint256 tokenId, 86 | uint8 layerType, 87 | bytes32 seed 88 | ) internal pure returns (uint16 layerSeed) { 89 | /// @solidity memory-safe-assembly 90 | assembly { 91 | // store seed in first slot of scratch memory 92 | mstore(0x00, seed) 93 | // pack tokenId and layerType into one 32-byte slot by shifting tokenId to the left 1 byte 94 | // tokenIds are sequential and MAX_NUM_SETS * NUM_TOKENS_PER_SET is guaranteed to be < 2**248 95 | let combinedIdType := or(shl(8, tokenId), layerType) 96 | mstore(0x20, combinedIdType) 97 | layerSeed := keccak256(0x00, 0x40) 98 | } 99 | } 100 | 101 | /** 102 | * @notice Determine layer type by its token ID 103 | */ 104 | function getLayerType(uint256 tokenId) 105 | public 106 | view 107 | virtual 108 | returns (uint8 layerType); 109 | 110 | /** 111 | * @notice Get the layerId for a given tokenId by hashing tokenId with its layer type and random seed, 112 | * and then comparing the final short against the appropriate distributions 113 | */ 114 | function getLayerId(uint256 tokenId) public view virtual returns (uint256) { 115 | return 116 | getLayerId( 117 | tokenId, 118 | getRandomnessForTokenIdFromSeed(tokenId, packedBatchRandomness) 119 | ); 120 | } 121 | 122 | /** 123 | * @dev perform fewer SLOADs by passing seed as parameter 124 | */ 125 | function getLayerId(uint256 tokenId, bytes32 seed) 126 | internal 127 | view 128 | virtual 129 | returns (uint256) 130 | { 131 | if (seed == 0) { 132 | revert BatchNotRevealed(); 133 | } 134 | uint8 layerType = getLayerType(tokenId); 135 | uint256 layerSeed = getLayerSeed(tokenId, layerType, seed); 136 | uint256[2] storage distributions = layerTypeToPackedDistributions[ 137 | layerType 138 | ]; 139 | return getLayerId(layerType, layerSeed, distributions); 140 | } 141 | 142 | /** 143 | * @notice calculate the layerId for a given layerType, seed, and distributions. 144 | * @param layerType of layer 145 | * @param layerSeed uint256 random seed for layer (in practice will be truncated to 8 bits) 146 | * @param distributionsArray uint256[2] packed distributions of layerIds 147 | * @return layerId limited to 8 bits 148 | * 149 | * @dev If the last packed short is <65535, any seed larger than the last packed short 150 | * will be assigned to the index after the last packed short, unless the last 151 | * packed short is index 31, in which case, it will default to 31. 152 | * LayerId is calculated like: index + 1 + 32 * layerType 153 | * 154 | * examples: 155 | * LayerSeed: 0x00 156 | * Distributions: [01 02 03 04 05 06 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00] 157 | * Calculated index: 0 (LayerId: 0 + 1 + 32 * layerType) 158 | * 159 | * LayerSeed: 0x01 160 | * Distributions: [01 02 03 04 05 06 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00] 161 | * Calculated index: 1 (LayerId: 1 + 1 + 32 * layerType) 162 | * 163 | * LayerSeed: 0xFF 164 | * Distributions: [01 02 03 04 05 06 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00] 165 | * Calculated index: 7 (LayerId: 7 + 1 + 32 * layerType) 166 | * 167 | * LayerSeed: 0xFF 168 | * Distributions: [01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20] 169 | * Calculated index: 31 (LayerId: 31 + 1 + 32 * layerType) 170 | */ 171 | function getLayerId( 172 | uint8 layerType, 173 | uint256 layerSeed, 174 | uint256[2] storage distributionsArray 175 | ) internal view returns (uint256 layerId) { 176 | /// @solidity memory-safe-assembly 177 | assembly { 178 | function revertWithBadDistributions() { 179 | mstore(0, BAD_DISTRIBUTIONS_SIGNATURE) 180 | revert(0, 4) 181 | } 182 | function getPackedShortFromLeft(index, packed) -> short { 183 | let shortOffset := sub(240, shl(4, index)) 184 | short := shr(shortOffset, packed) 185 | short := and(short, 0xffff) 186 | } 187 | 188 | let j 189 | // declare i outside of loop in case final distribution val is less than seed 190 | let i 191 | let jOffset 192 | let indexOffset 193 | 194 | // iterate over distribution values until we find one that our layer seed is less than 195 | for { 196 | 197 | } lt(j, 2) { 198 | j := add(1, j) 199 | indexOffset := add(indexOffset, 0x20) 200 | i := 0 201 | } { 202 | // lazily load each half of distributions from storage, since we might not need the second half 203 | let distributions := sload(add(distributionsArray.slot, j)) 204 | jOffset := shl(4, j) 205 | 206 | for { 207 | 208 | } lt(i, 16) { 209 | i := add(1, i) 210 | } { 211 | let dist := getPackedShortFromLeft(i, distributions) 212 | if iszero(dist) { 213 | if iszero(i) { 214 | if iszero(j) { 215 | // first element should never be 0; distributions are invalid 216 | revertWithBadDistributions() 217 | } 218 | } 219 | // if we've reached end of distributions, check layer type != 7 220 | // otherwise if layerSeed is less than the last distribution, 221 | // the layerId calculation will evaluate to 256 (overflow) 222 | if eq(layerType, 7) { 223 | if eq(add(i, jOffset), 31) { 224 | revertWithBadDistributions() 225 | } 226 | } 227 | // if distribution is 0, and it's not the first, we've reached the end of the list 228 | // return i + 1 + 32 * layerType 229 | layerId := add( 230 | // add 1 if j == 0 231 | // add 17 if j == 1 232 | add(i, add(1, jOffset)), 233 | shl(5, layerType) 234 | ) 235 | break 236 | } 237 | if lt(layerSeed, dist) { 238 | // if i+jOffset is 31 here, math will overflow here if layerType == 7 239 | // 31 + 1 + 32 * 7 = 256, which is too large for a uint8 240 | if eq(layerType, 7) { 241 | if eq(add(i, jOffset), 31) { 242 | revertWithBadDistributions() 243 | } 244 | } 245 | 246 | // layerIds are 1-indexed, so add 1 to i+j 247 | layerId := add( 248 | // add 1 if j == 0 249 | // add 17 if j == 1 250 | add(i, add(1, jOffset)), 251 | shl(5, layerType) 252 | ) 253 | break 254 | } 255 | } 256 | // if layerId has been set, we don't need to increment j 257 | if gt(layerId, 0) { 258 | break 259 | } 260 | } 261 | // if i+j is 32, we've reached the end of the list and should default to the last id 262 | if iszero(layerId) { 263 | if eq(j, 2) { 264 | // math will overflow here if layerType == 7 265 | // 32 + 32 * 7 = 256, which is too large for a uint8 266 | if eq(layerType, 7) { 267 | revertWithBadDistributions() 268 | } 269 | // return previous layerId 270 | layerId := add(32, shl(5, layerType)) 271 | } 272 | } 273 | } 274 | } 275 | 276 | function _setLayerTypeDistribution( 277 | uint8 layerType, 278 | uint256[2] calldata distribution 279 | ) internal { 280 | if (layerType > 7) { 281 | revert InvalidLayerType(); 282 | } 283 | layerTypeToPackedDistributions[layerType] = distribution; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/traits/RandomTraitsImpl.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {RandomTraits} from './RandomTraits.sol'; 5 | 6 | abstract contract RandomTraitsImpl is RandomTraits { 7 | /** 8 | * @notice Determine layer type by its token ID 9 | */ 10 | function getLayerType(uint256 tokenId) 11 | public 12 | view 13 | virtual 14 | override 15 | returns (uint8 layerType) 16 | { 17 | uint256 numTokensPerSet = NUM_TOKENS_PER_SET; 18 | 19 | /// @solidity memory-safe-assembly 20 | assembly { 21 | layerType := mod(tokenId, numTokensPerSet) 22 | if gt(layerType, 5) { 23 | layerType := 5 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/vrf/BatchVRFConsumer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.4; 3 | 4 | import {VRFConsumerBaseV2} from 'chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol'; 5 | import {VRFCoordinatorV2Interface} from 'chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol'; 6 | import {TwoStepOwnable} from 'utility-contracts/TwoStepOwnable.sol'; 7 | import {ERC721A} from '../token/ERC721A.sol'; 8 | import {_32_MASK, BATCH_NOT_REVEALED_SIGNATURE} from '../interface/Constants.sol'; 9 | import {MaxRandomness, NumRandomBatchesMustBeLessThanOrEqualTo16, NoBatchesToReveal, RevealPending, OnlyCoordinatorCanFulfill, UnsafeReveal, NumRandomBatchesMustBePowerOfTwo, NumRandomBatchesMustBeGreaterThanOne} from '../interface/Errors.sol'; 10 | import {BitMapUtility} from '../lib/BitMapUtility.sol'; 11 | import {PackedByteUtility} from '../lib/PackedByteUtility.sol'; 12 | 13 | contract BatchVRFConsumer is ERC721A, TwoStepOwnable { 14 | // VRF config 15 | uint256 public immutable NUM_RANDOM_BATCHES; 16 | uint256 public immutable BITS_PER_RANDOM_BATCH; 17 | uint256 immutable BITS_PER_BATCH_SHIFT; 18 | uint256 immutable BATCH_RANDOMNESS_MASK; 19 | 20 | uint16 constant NUM_CONFIRMATIONS = 7; 21 | uint32 constant CALLBACK_GAS_LIMIT = 500_000; 22 | uint64 public subscriptionId; 23 | VRFCoordinatorV2Interface public coordinator; 24 | 25 | // token config 26 | // use uint240 to ensure tokenId can never be > 2**248 for efficient hashing 27 | uint240 immutable MAX_NUM_SETS; 28 | uint8 immutable NUM_TOKENS_PER_SET; 29 | uint248 immutable NUM_TOKENS_PER_RANDOM_BATCH; 30 | uint256 immutable MAX_TOKEN_ID; 31 | 32 | bytes32 public packedBatchRandomness; 33 | uint248 revealBatch; 34 | bool public pendingReveal; 35 | bytes32 public keyHash; 36 | 37 | // allow unsafe revealing of an uncompleted batch, ie, in the case of a stalled mint 38 | bool forceUnsafeReveal; 39 | 40 | constructor( 41 | string memory name, 42 | string memory symbol, 43 | address vrfCoordinatorAddress, 44 | uint240 maxNumSets, 45 | uint8 numTokensPerSet, 46 | uint64 _subscriptionId, 47 | uint8 numRandomBatches, 48 | bytes32 _keyHash 49 | ) ERC721A(name, symbol) { 50 | if (numRandomBatches < 2) { 51 | revert NumRandomBatchesMustBeGreaterThanOne(); 52 | } else if (numRandomBatches > 16) { 53 | revert NumRandomBatchesMustBeLessThanOrEqualTo16(); 54 | } 55 | // store immutables to allow for configurable number of random batches 56 | // (which must be a power of two), with inversely proportional amounts of 57 | // entropy per batch. 58 | // 16 batches (16 bits of entropy per batch) is the max recommended 59 | // 2 batches is the minimum 60 | NUM_RANDOM_BATCHES = numRandomBatches; 61 | BITS_PER_RANDOM_BATCH = uint8(uint256(256) / NUM_RANDOM_BATCHES); 62 | BITS_PER_BATCH_SHIFT = uint8( 63 | BitMapUtility.msb(uint256(BITS_PER_RANDOM_BATCH)) 64 | ); 65 | bool powerOfTwo = uint256(BITS_PER_RANDOM_BATCH) * 66 | uint256(NUM_RANDOM_BATCHES) == 67 | 256; 68 | if (!powerOfTwo) { 69 | revert NumRandomBatchesMustBePowerOfTwo(); 70 | } 71 | BATCH_RANDOMNESS_MASK = ((1 << BITS_PER_RANDOM_BATCH) - 1); 72 | 73 | MAX_NUM_SETS = maxNumSets; 74 | NUM_TOKENS_PER_SET = numTokensPerSet; 75 | 76 | // ensure that the last batch includes the very last token ids 77 | uint248 numSetsPerRandomBatch = uint248(MAX_NUM_SETS) / 78 | uint248(NUM_RANDOM_BATCHES); 79 | uint256 recoveredNumSets = (numSetsPerRandomBatch * NUM_RANDOM_BATCHES); 80 | if (recoveredNumSets != MAX_NUM_SETS) { 81 | ++numSetsPerRandomBatch; 82 | } 83 | // use numSetsPerRandomBatch to calculate the number of tokens per batch 84 | // to avoid revealing only some tokens in a set 85 | NUM_TOKENS_PER_RANDOM_BATCH = 86 | numSetsPerRandomBatch * 87 | NUM_TOKENS_PER_SET; 88 | 89 | MAX_TOKEN_ID = 90 | _startTokenId() + 91 | uint256(MAX_NUM_SETS) * 92 | uint256(NUM_TOKENS_PER_SET) - 93 | 1; 94 | 95 | coordinator = VRFCoordinatorV2Interface(vrfCoordinatorAddress); 96 | subscriptionId = _subscriptionId; 97 | keyHash = _keyHash; 98 | } 99 | 100 | /** 101 | * @notice when true, allow revealing the rest of a batch that has not completed minting yet 102 | * This is "unsafe" because it becomes possible to know the layerIds of unminted tokens from the batch 103 | */ 104 | function setForceUnsafeReveal(bool force) external onlyOwner { 105 | forceUnsafeReveal = force; 106 | } 107 | 108 | /** 109 | * @notice set the key hash corresponding to a max gas price for a chainlink VRF request, 110 | * to be used in requestRandomWords() 111 | */ 112 | function setKeyHash(bytes32 _keyHash) external onlyOwner { 113 | keyHash = _keyHash; 114 | } 115 | 116 | /** 117 | * @notice set the ChainLink VRF Subscription ID 118 | */ 119 | function setSubscriptionId(uint64 _subscriptionId) external onlyOwner { 120 | subscriptionId = _subscriptionId; 121 | } 122 | 123 | /** 124 | * @notice set the ChainLink VRF Coordinator address 125 | */ 126 | function setCoordinator(address _coordinator) external onlyOwner { 127 | coordinator = VRFCoordinatorV2Interface(_coordinator); 128 | } 129 | 130 | /** 131 | * @notice Clear the pending reveal flag, allowing requestRandomWords() to be called again 132 | */ 133 | function clearPendingReveal() external onlyOwner { 134 | pendingReveal = false; 135 | } 136 | 137 | /** 138 | * @notice request random words from the chainlink vrf for each unrevealed batch 139 | */ 140 | function requestRandomWords() external returns (uint256) { 141 | if (pendingReveal) { 142 | revert RevealPending(); 143 | } 144 | (uint32 numBatches, ) = _checkAndReturnNumBatches(); 145 | if (numBatches == 0) { 146 | revert NoBatchesToReveal(); 147 | } 148 | 149 | // Will revert if subscription is not set and funded. 150 | uint256 _pending = coordinator.requestRandomWords( 151 | keyHash, 152 | subscriptionId, 153 | NUM_CONFIRMATIONS, 154 | CALLBACK_GAS_LIMIT, 155 | 1 156 | ); 157 | pendingReveal = true; 158 | return _pending; 159 | } 160 | 161 | /** 162 | * @notice get the random seed of the batch that a given token ID belongs to 163 | */ 164 | function getRandomnessForTokenId(uint256 tokenId) 165 | internal 166 | view 167 | returns (bytes32 randomness) 168 | { 169 | return getRandomnessForTokenIdFromSeed(tokenId, packedBatchRandomness); 170 | } 171 | 172 | /** 173 | * @notice Get the randomness for a given tokenId, if it's been set 174 | * @param tokenId tokenId of the token to get the randomness for 175 | * @param seed bytes32 seed containing all batches randomness 176 | * @return randomness as bytes32 for the given tokenId 177 | */ 178 | function getRandomnessForTokenIdFromSeed(uint256 tokenId, bytes32 seed) 179 | internal 180 | view 181 | returns (bytes32 randomness) 182 | { 183 | // put immutable variable onto stack 184 | uint256 numTokensPerRandomBatch = NUM_TOKENS_PER_RANDOM_BATCH; 185 | uint256 shift = BITS_PER_BATCH_SHIFT; 186 | uint256 mask = BATCH_RANDOMNESS_MASK; 187 | 188 | /// @solidity memory-safe-assembly 189 | assembly { 190 | // use mask to get last N bits of shifted packedBatchRandomness 191 | randomness := and( 192 | // shift packedBatchRandomness right by batchNum * bits per batch 193 | shr( 194 | // get batch number of token, multiply by bits per batch 195 | shl(shift, div(tokenId, numTokensPerRandomBatch)), 196 | seed 197 | ), 198 | mask 199 | ) 200 | } 201 | } 202 | 203 | // rawFulfillRandomness is called by VRFCoordinator when it receives a valid VRF 204 | // proof. rawFulfillRandomness then calls fulfillRandomness, after validating 205 | // the origin of the call 206 | function rawFulfillRandomWords( 207 | uint256 requestId, 208 | uint256[] memory randomWords 209 | ) external { 210 | if (msg.sender != address(coordinator)) { 211 | revert OnlyCoordinatorCanFulfill(msg.sender, address(coordinator)); 212 | } 213 | fulfillRandomWords(requestId, randomWords); 214 | } 215 | 216 | /** 217 | * @notice fulfillRandomness handles the VRF response. Your contract must 218 | * @notice implement it. See "SECURITY CONSIDERATIONS" above for important 219 | * @notice principles to keep in mind when implementing your fulfillRandomness 220 | * @notice method. 221 | * 222 | * @dev VRFConsumerBaseV2 expects its subcontracts to have a method with this 223 | * @dev signature, and will call it once it has verified the proof 224 | * @dev associated with the randomness. (It is triggered via a call to 225 | * @dev rawFulfillRandomness, below.) 226 | * 227 | * @param 228 | * @param randomWords the VRF output expanded to the requested number of words 229 | */ 230 | function fulfillRandomWords(uint256, uint256[] memory randomWords) 231 | internal 232 | virtual 233 | { 234 | (uint32 numBatches, uint32 _revealBatch) = _checkAndReturnNumBatches(); 235 | uint256 currSeed = uint256(packedBatchRandomness); 236 | uint256 randomness = randomWords[0]; 237 | 238 | // we have revealed N batches; mask the bottom bits out 239 | uint256 mask; 240 | uint256 bitShift = BITS_PER_RANDOM_BATCH * _revealBatch; 241 | // solidity will overflow and throw arithmetic error without this check 242 | if (bitShift != 256) { 243 | // will be 0 if bitshift == 256 (and would not overflow) 244 | mask = type(uint256).max ^ ((1 << bitShift) - 1); 245 | } 246 | // we need only need to reveal up to M batches; mask the top bits out 247 | bitShift = (BITS_PER_RANDOM_BATCH * (numBatches + _revealBatch)); 248 | if (bitShift != 256) { 249 | mask = mask & ((1 << bitShift) - 1); 250 | } 251 | 252 | uint256 newRandomness = randomness & mask; 253 | currSeed = currSeed | newRandomness; 254 | 255 | _revealBatch += numBatches; 256 | 257 | // coerce any 0-slots to 1 258 | for (uint256 i; i < numBatches; ) { 259 | uint256 retrievedRandomness = PackedByteUtility.getPackedNFromRight( 260 | uint256(currSeed), 261 | BITS_PER_RANDOM_BATCH, 262 | i 263 | ); 264 | if (retrievedRandomness == 0) { 265 | currSeed = PackedByteUtility.packNAtRightIndex( 266 | uint256(currSeed), 267 | BITS_PER_RANDOM_BATCH, 268 | 1, 269 | i 270 | ); 271 | } 272 | unchecked { 273 | ++i; 274 | } 275 | } 276 | 277 | packedBatchRandomness = bytes32(currSeed); 278 | revealBatch = _revealBatch; 279 | pendingReveal = false; 280 | } 281 | 282 | /** 283 | * @notice calculate how many batches need to be revealed, and also get next batch number 284 | * @return (uint32 numMissingBatches, uint32 _revealBatch) - number missing batches, and the current _revealBatch 285 | * index (current batch revealed + 1, or 0 if none) 286 | */ 287 | function _checkAndReturnNumBatches() 288 | internal 289 | view 290 | returns (uint32, uint32) 291 | { 292 | // get next unminted token ID 293 | uint256 nextTokenId_ = _nextTokenId(); 294 | // get number of fully completed batches 295 | uint256 numCompletedBatches = nextTokenId_ / 296 | NUM_TOKENS_PER_RANDOM_BATCH; 297 | 298 | // if NUM_TOKENS_PER_RANDOM_BATCH doesn't divide evenly into total number of tokens, 299 | // increment the numCompleted batches if the next token ID is greater than the max 300 | // ie, the very last batch is completed 301 | // NUM_TOKENS_PER_RANDOM_BATCH * NUM_RANDOM_BATCHES / NUM_TOKENS_PER_SET will always 302 | // either be greater than or equal to MAX_NUM_SETS, never less-than 303 | bool unevenBatches = ((NUM_TOKENS_PER_RANDOM_BATCH * 304 | NUM_RANDOM_BATCHES) / NUM_TOKENS_PER_SET) != MAX_NUM_SETS; 305 | if (unevenBatches && nextTokenId_ > MAX_TOKEN_ID) { 306 | ++numCompletedBatches; 307 | } 308 | 309 | uint32 _revealBatch = uint32(revealBatch); 310 | // reveal is complete if _revealBatch is >= 8 311 | if (_revealBatch >= NUM_RANDOM_BATCHES) { 312 | revert MaxRandomness(); 313 | } 314 | 315 | // if equal, next batch has not started minting yet 316 | bool batchIsInProgress = nextTokenId_ > 317 | numCompletedBatches * NUM_TOKENS_PER_RANDOM_BATCH && 318 | numCompletedBatches != NUM_RANDOM_BATCHES; 319 | bool batchInProgressAlreadyRevealed = _revealBatch > 320 | numCompletedBatches; 321 | uint32 numMissingBatches = batchInProgressAlreadyRevealed 322 | ? 0 323 | : uint32(numCompletedBatches) - _revealBatch; 324 | 325 | // don't ever reveal batches from which no tokens have been minted 326 | if ( 327 | batchInProgressAlreadyRevealed || 328 | (numMissingBatches == 0 && !batchIsInProgress) 329 | ) { 330 | revert UnsafeReveal(); 331 | } 332 | // increment if batch is in progress 333 | if (batchIsInProgress && forceUnsafeReveal) { 334 | ++numMissingBatches; 335 | } 336 | 337 | return (numMissingBatches, _revealBatch); 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /test/BoundLayerableFuzz.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {BoundLayerableTestImpl} from 'bound-layerable/implementations/BoundLayerableTestImpl.sol'; 6 | import {PackedByteUtility} from 'bound-layerable/lib/PackedByteUtility.sol'; 7 | import {LayerVariation} from 'bound-layerable/interface/Structs.sol'; 8 | import {BoundLayerableEvents} from 'bound-layerable/interface/Events.sol'; 9 | import {ArrayLengthMismatch, LayerNotBoundToTokenId, MultipleVariationsEnabled, DuplicateActiveLayers} from 'bound-layerable/interface/Errors.sol'; 10 | 11 | contract BoundLayerableFuzzTest is Test, BoundLayerableEvents { 12 | BoundLayerableTestImpl test; 13 | 14 | function setUp() public { 15 | test = new BoundLayerableTestImpl(); 16 | test.mint(); 17 | test.mint(); 18 | test.mint(); 19 | test.setBoundLayers(14, 2**256 - 1); 20 | test.setPackedBatchRandomness(bytes32(bytes1(0x01))); 21 | uint256[] memory layers = new uint256[](2); 22 | layers[0] = 1; 23 | layers[1] = 2; 24 | vm.startPrank(address(1)); 25 | test.mint(); 26 | for (uint256 i = 0; i < 7; i++) { 27 | test.transferFrom(address(1), address(this), i + 21); 28 | } 29 | vm.stopPrank(); 30 | } 31 | 32 | function testFuzzCheckUnpackedIsSubsetOfBound( 33 | uint256 superset, 34 | uint256 subset 35 | ) public { 36 | // create perfect superset 37 | uint256 originalSuperset = superset; 38 | superset |= subset; 39 | // check should not revert 40 | test.checkUnpackedIsSubsetOfBound(subset, superset); 41 | 42 | // create bad superset 43 | uint256 badSuperSet = originalSuperset &= subset; 44 | // if they're equal, add 1 bit to subset 45 | // unless not possible, in which case, swap the two and subtract 1 bit from badsuper 46 | if (badSuperSet == subset) { 47 | if (subset != type(uint256).max) { 48 | subset += 1; 49 | } else { 50 | badSuperSet = subset - 1; 51 | } 52 | } 53 | uint256 expectedErrorParam = subset & (badSuperSet ^ subset); 54 | // check should revert 55 | vm.expectRevert( 56 | abi.encodeWithSelector( 57 | LayerNotBoundToTokenId.selector, 58 | expectedErrorParam 59 | ) 60 | ); 61 | test.checkUnpackedIsSubsetOfBound(subset, badSuperSet); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/BoundLayerableSnapshot.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {BoundLayerableSnapshotImpl} from 'bound-layerable/implementations/BoundLayerableSnapshotImpl.sol'; 6 | import {PackedByteUtility} from 'bound-layerable/lib/PackedByteUtility.sol'; 7 | import {BitMapUtility} from 'bound-layerable/lib/BitMapUtility.sol'; 8 | 9 | import {LayerVariation} from 'bound-layerable/interface/Structs.sol'; 10 | import {BoundLayerableEvents} from 'bound-layerable/interface/Events.sol'; 11 | import {ArrayLengthMismatch, LayerNotBoundToTokenId, MultipleVariationsEnabled, DuplicateActiveLayers} from 'bound-layerable/interface/Errors.sol'; 12 | 13 | contract BoundLayerableSnapshotTest is Test, BoundLayerableEvents { 14 | BoundLayerableSnapshotImpl test; 15 | 16 | function setUp() public { 17 | test = new BoundLayerableSnapshotImpl(); 18 | test.mint(); 19 | test.mint(); 20 | test.mint(); 21 | test.setBoundLayers(14, 2**256 - 1); 22 | test.setPackedBatchRandomness(bytes32(uint256(2**256 - 1))); 23 | uint256[] memory layers = new uint256[](2); 24 | layers[0] = 1; 25 | layers[1] = 2; 26 | vm.startPrank(address(1)); 27 | test.mint(); 28 | for (uint256 i = 0; i < 7; i++) { 29 | test.transferFrom(address(1), address(this), i + 21); 30 | } 31 | vm.stopPrank(); 32 | } 33 | 34 | function test_snapshotSetActiveLayers() public { 35 | test.setActiveLayers(14, ((14 << 248) | (15 << 240) | (16 << 232))); 36 | } 37 | 38 | function test_snapshotBurnAndBindMultiple1() public { 39 | uint256[] memory layers = new uint256[](6); 40 | layers[0] = 6; 41 | layers[1] = 1; 42 | layers[2] = 2; 43 | layers[3] = 3; 44 | layers[4] = 4; 45 | layers[5] = 5; 46 | 47 | test.burnAndBindMultiple(0, layers); 48 | } 49 | 50 | function test_snapshotBurnAndBindMultipleAndSetActive() public { 51 | uint256[] memory layers = new uint256[](6); 52 | layers[0] = 6; 53 | layers[1] = 1; 54 | layers[2] = 2; 55 | layers[3] = 3; 56 | layers[4] = 4; 57 | layers[5] = 5; 58 | uint256 boundLayerBitMap = 769242387287835449923186861691057117988303463679787008; 59 | uint256[] memory individualLayers = BitMapUtility.unpackBitMap( 60 | boundLayerBitMap 61 | ); 62 | uint256 packed = PackedByteUtility.packArrayOfBytes(individualLayers); 63 | 64 | test.burnAndBindMultipleAndSetActiveLayers(0, layers, packed); 65 | } 66 | 67 | function test_snapshotBurnAndBindSingleTransferred() public { 68 | test.burnAndBindSingle(0, 22); 69 | } 70 | 71 | function test_snapshotBurnAndBindMultipleTransferred() public { 72 | uint256[] memory layers = new uint256[](6); 73 | layers[0] = 22; 74 | layers[1] = 23; 75 | layers[2] = 24; 76 | layers[3] = 25; 77 | layers[4] = 26; 78 | layers[5] = 27; 79 | test.burnAndBindMultiple(21, layers); 80 | } 81 | 82 | function test_snapshotBurnAndBindSingle() public { 83 | test.burnAndBindSingle(0, 1); 84 | } 85 | 86 | function test_snapshotMintSingle() public { 87 | test.mint(); 88 | } 89 | 90 | function test_snapshotMintFive() public { 91 | test.mint(5); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/Token.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {TestToken} from 'bound-layerable/implementations/TestToken.sol'; 6 | 7 | import {PackedByteUtility} from 'bound-layerable/lib/PackedByteUtility.sol'; 8 | import {RandomTraits} from 'bound-layerable/traits/RandomTraits.sol'; 9 | import {ERC721Recipient} from './util/ERC721Recipient.sol'; 10 | import {LayerType} from 'bound-layerable/interface/Enums.sol'; 11 | import {BitMapUtility} from 'bound-layerable/lib/BitMapUtility.sol'; 12 | 13 | contract TestTokenTest is Test, ERC721Recipient { 14 | TestToken test; 15 | uint256[] distributions; 16 | 17 | function setUp() public virtual { 18 | test = new TestToken('Test', 'test', ''); 19 | test.setPackedBatchRandomness(bytes32(uint256(1))); 20 | } 21 | 22 | function testDoTheMost() public { 23 | // // todo: set rarities 24 | 25 | // 6 backgrounds 26 | distributions = [ 27 | uint256(42 * 256), 28 | uint256(84 * 256), 29 | uint256(126 * 256), 30 | uint256(168 * 256), 31 | uint256(210 * 256), 32 | uint256(252 * 256) 33 | ]; 34 | 35 | uint256[] memory _distributions = distributions; 36 | uint256[2] memory packedDistributions = PackedByteUtility 37 | .packArrayOfShorts(_distributions); 38 | test.setLayerTypeDistribution( 39 | uint8(LayerType.BACKGROUND), 40 | packedDistributions 41 | ); 42 | 43 | packedDistributions = [uint256(2**16 - 1) << 240, uint256(0)]; 44 | 45 | // 1 portrait 46 | test.setLayerTypeDistribution( 47 | uint8(LayerType.PORTRAIT), 48 | packedDistributions 49 | ); 50 | 51 | // 5 textures 52 | distributions = [ 53 | uint256(51 * 256), 54 | uint256(102 * 256), 55 | uint256(153 * 256), 56 | uint256(204 * 256), 57 | uint256(255 * 256) 58 | ]; 59 | _distributions = distributions; 60 | packedDistributions = PackedByteUtility.packArrayOfShorts( 61 | _distributions 62 | ); 63 | 64 | test.setLayerTypeDistribution( 65 | uint8(LayerType.TEXTURE), 66 | packedDistributions 67 | ); 68 | 69 | // 8 objects 70 | distributions = [ 71 | uint256(31 * 256), 72 | uint256(62 * 256), 73 | uint256(93 * 256), 74 | uint256(124 * 256), 75 | uint256(155 * 256), 76 | uint256(186 * 256), 77 | uint256(217 * 256), 78 | uint256(248 * 256) 79 | ]; 80 | _distributions = distributions; 81 | packedDistributions = PackedByteUtility.packArrayOfShorts( 82 | _distributions 83 | ); 84 | test.setLayerTypeDistribution( 85 | uint8(LayerType.OBJECT), 86 | packedDistributions 87 | ); 88 | 89 | // 7 borders 90 | distributions = [ 91 | uint256(36 * 256), 92 | uint256(72 * 256), 93 | uint256(108 * 256), 94 | uint256(144 * 256), 95 | uint256(180 * 256), 96 | uint256(216 * 256), 97 | uint256(252 * 256) 98 | ]; 99 | _distributions = distributions; 100 | packedDistributions = PackedByteUtility.packArrayOfShorts( 101 | _distributions 102 | ); 103 | test.setLayerTypeDistribution( 104 | uint8(LayerType.BORDER), 105 | packedDistributions 106 | ); 107 | 108 | // test.setBaseLayerURI( 109 | // '/Users/jameswenzel/dev/partner-smart-contracts/Layers/' 110 | // ); 111 | 112 | // // do the thing 113 | 114 | uint256 tokenId = 6; 115 | 116 | test.mintSet(); 117 | uint256 startingTokenId = tokenId * 7; 118 | 119 | // get layerIds from token IDs 120 | uint256[] memory layers = new uint256[](7); 121 | for ( 122 | uint256 layerTokenId = startingTokenId; 123 | layerTokenId < startingTokenId + 7; 124 | layerTokenId++ 125 | ) { 126 | uint256 layer = test.getLayerId(layerTokenId); 127 | emit log_named_uint('layer', layer); 128 | uint256 lastLayer = 0; 129 | if (layerTokenId > startingTokenId) { 130 | lastLayer = layers[(layerTokenId % 7) - 1]; 131 | } 132 | if (layer == lastLayer) { 133 | emit log('oops'); 134 | layer += 1; 135 | } 136 | layers[layerTokenId % 7] = uint256(layer); 137 | emit log_named_uint('copied layer', layers[layerTokenId % 7]); 138 | } 139 | 140 | // create copy as uint256 bc todo: i need to fix 141 | uint256 packedLayers = PackedByteUtility.packArrayOfBytes(layers); 142 | 143 | emit log_named_uint('packedLayers', packedLayers); 144 | 145 | uint256 binding = BitMapUtility.uintsToBitMap(layers); 146 | 147 | emit log_named_uint('binding', binding); 148 | test.setBoundLayers(tokenId * 7, binding); 149 | 150 | // swap layer ordering 151 | uint256 temp = layers[0]; 152 | layers[0] = layers[1]; 153 | layers[1] = temp; 154 | uint256 newPackedLayers = PackedByteUtility.packArrayOfBytes(layers); 155 | // set active layers - use portrait id, not b 156 | test.setActiveLayers(startingTokenId, newPackedLayers); 157 | uint256[] memory activeLayers = test.getActiveLayers(startingTokenId); 158 | for (uint256 i; i < activeLayers.length; i++) { 159 | emit log_named_uint('activeLayer', activeLayers[i]); 160 | } // emit log(test.metadataContract().getTokenSVG(startingTokenId)); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /test/TokenBulkBurn.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {TestToken} from 'bound-layerable/implementations/TestToken.sol'; 6 | import {PackedByteUtility} from 'bound-layerable/lib/PackedByteUtility.sol'; 7 | import {RandomTraits} from 'bound-layerable/traits/RandomTraits.sol'; 8 | import {ERC721Recipient} from './util/ERC721Recipient.sol'; 9 | import {LayerType} from 'bound-layerable/interface/Enums.sol'; 10 | 11 | contract TokenImpl is TestToken { 12 | constructor( 13 | string memory name, 14 | string memory sym, 15 | string memory idk 16 | ) TestToken(name, sym, idk) {} 17 | 18 | function setBoundLayersBulkNoCalldataOverhead() public { 19 | // TODO: check tokenIds are valid? 20 | 21 | for (uint256 i; i < 5555; ) { 22 | _tokenIdToBoundLayers[i * 7] = 2; 23 | unchecked { 24 | ++i; 25 | } 26 | } 27 | } 28 | } 29 | 30 | contract TokenBulkBurnTest is Test, ERC721Recipient { 31 | TokenImpl test; 32 | uint8[] distributions; 33 | 34 | function setUp() public virtual { 35 | test = new TokenImpl('Test', 'test', ''); 36 | test.mintSets(5555); 37 | test.setPackedBatchRandomness(bytes32(uint256(1))); 38 | } 39 | 40 | function test_snapshotDisableTradingAndBurn() public { 41 | test.disableTrading(); 42 | } 43 | 44 | function test_snapshotBulkBindLayers() public { 45 | test.setBoundLayersBulkNoCalldataOverhead(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/examples/BoundLayerableFirstComposedCutoffSnapshot.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {BoundLayerableFirstComposedCutoffImpl} from 'bound-layerable/implementations/BoundLayerableFirstComposedCutoffImpl.sol'; 6 | import {PackedByteUtility} from 'bound-layerable/lib/PackedByteUtility.sol'; 7 | import {LayerVariation} from 'bound-layerable/interface/Structs.sol'; 8 | import {BoundLayerableEvents} from 'bound-layerable/interface/Events.sol'; 9 | import {ArrayLengthMismatch, LayerNotBoundToTokenId, MultipleVariationsEnabled, DuplicateActiveLayers} from 'bound-layerable/interface/Errors.sol'; 10 | 11 | contract BoundLayerableFirstComposedCutoffSnapshotTest is 12 | Test, 13 | BoundLayerableEvents 14 | { 15 | BoundLayerableFirstComposedCutoffImpl test; 16 | 17 | function setUp() public { 18 | test = new BoundLayerableFirstComposedCutoffImpl(); 19 | test.mint(); 20 | test.mint(); 21 | test.mint(); 22 | test.setBoundLayers(14, 2**256 - 1); 23 | test.setPackedBatchRandomness(bytes32(uint256(2**256 - 1))); 24 | uint256[] memory layers = new uint256[](2); 25 | layers[0] = 1; 26 | layers[1] = 2; 27 | vm.startPrank(address(1)); 28 | test.mint(); 29 | for (uint256 i = 0; i < 7; i++) { 30 | test.transferFrom(address(1), address(this), i + 21); 31 | } 32 | vm.stopPrank(); 33 | } 34 | 35 | function test_snapshotSetActiveLayers() public { 36 | test.setActiveLayers(14, ((14 << 248) | (15 << 240) | (16 << 232))); 37 | } 38 | 39 | function test_snapshotBurnAndBindMultiple1() public { 40 | uint256[] memory layers = new uint256[](6); 41 | layers[0] = 6; 42 | layers[1] = 1; 43 | layers[2] = 2; 44 | layers[3] = 3; 45 | layers[4] = 4; 46 | layers[5] = 5; 47 | 48 | test.burnAndBindMultiple(0, layers); 49 | } 50 | 51 | function test_snapshotBurnAndBindSingleTransferred() public { 52 | test.burnAndBindSingle(0, 22); 53 | } 54 | 55 | function test_snapshotBurnAndBindMultipleTransferred() public { 56 | uint256[] memory layers = new uint256[](6); 57 | layers[0] = 22; 58 | layers[1] = 23; 59 | layers[2] = 24; 60 | layers[3] = 25; 61 | layers[4] = 26; 62 | layers[5] = 27; 63 | test.burnAndBindMultiple(21, layers); 64 | } 65 | 66 | function test_snapshotBurnAndBindSingle() public { 67 | test.burnAndBindSingle(0, 1); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/helpers/StringTestUtility.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | library StringTestUtility { 5 | function startsWith(string memory ref, string memory test) 6 | internal 7 | pure 8 | returns (bool) 9 | { 10 | bytes memory refBytes = bytes(ref); 11 | bytes memory testBytes = bytes(test); 12 | if (testBytes.length > refBytes.length) { 13 | return false; 14 | } 15 | for (uint256 i = 0; i < testBytes.length; ++i) { 16 | if (refBytes[i] != testBytes[i]) { 17 | return false; 18 | } 19 | } 20 | return true; 21 | } 22 | 23 | function endsWith(string memory ref, string memory test) 24 | internal 25 | pure 26 | returns (bool) 27 | { 28 | bytes memory refBytes = bytes(ref); 29 | bytes memory testBytes = bytes(test); 30 | if (testBytes.length > refBytes.length) { 31 | return false; 32 | } 33 | for (uint256 i = 0; i < testBytes.length; ++i) { 34 | if ( 35 | refBytes[refBytes.length - 1 - i] != 36 | testBytes[testBytes.length - 1 - i] 37 | ) { 38 | return false; 39 | } 40 | } 41 | return true; 42 | } 43 | 44 | function countChar(string memory str, bytes1 c) 45 | internal 46 | pure 47 | returns (uint256) 48 | { 49 | uint256 count; 50 | bytes memory strBytes = bytes(str); 51 | for (uint256 i = 0; i < strBytes.length; ++i) { 52 | if (strBytes[i] == c) { 53 | ++count; 54 | } 55 | } 56 | return count; 57 | } 58 | 59 | function contains(string memory str, string memory test) 60 | internal 61 | pure 62 | returns (bool) 63 | { 64 | bytes memory strBytes = bytes(str); 65 | bytes memory testBytes = bytes(test); 66 | if (testBytes.length > strBytes.length) { 67 | return false; 68 | } 69 | uint256 strBytesLength = strBytes.length; 70 | uint256 testBytesLength = testBytes.length; 71 | for (uint256 i = 0; i < strBytesLength - testBytesLength + 1; ++i) { 72 | if (testBytesLength > strBytesLength - i) { 73 | return false; 74 | } 75 | string memory spliced = splice(str, i, strBytesLength - 1); 76 | bool found = startsWith(spliced, test); 77 | if (found) { 78 | return found; 79 | } 80 | } 81 | return false; 82 | } 83 | 84 | function contains(string memory str, bytes1 char) 85 | internal 86 | pure 87 | returns (bool) 88 | { 89 | return countChar(str, char) > 0; 90 | } 91 | 92 | function splice( 93 | string memory str, 94 | uint256 start, 95 | uint256 end 96 | ) internal pure returns (string memory) { 97 | bytes memory strBytes = bytes(str); 98 | uint256 resultLength = end - start + 1; 99 | bytes memory resultBytes = new bytes(resultLength); 100 | for (uint256 i = 0; i < resultLength; ++i) { 101 | resultBytes[i] = strBytes[i + start]; 102 | } 103 | return string(resultBytes); 104 | } 105 | 106 | function equals(string memory ref, string memory test) 107 | internal 108 | pure 109 | returns (bool) 110 | { 111 | return keccak256(abi.encode(ref)) == keccak256(abi.encode(test)); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/helpers/StringTestUtility.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {StringTestUtility} from './StringTestUtility.sol'; 6 | 7 | contract StringtestUtilityTest is Test { 8 | using StringTestUtility for string; 9 | 10 | function testStartsWith() public { 11 | string memory test = 'test'; 12 | string memory ref = 'test'; 13 | assertTrue(ref.startsWith(test)); 14 | ref = 'test2'; 15 | assertTrue(ref.startsWith(test)); 16 | test = 'test3'; 17 | assertFalse(ref.startsWith(test)); 18 | } 19 | 20 | function testEndsWith() public { 21 | string memory test = 'test'; 22 | string memory ref = 'test'; 23 | assertTrue(ref.endsWith(test)); 24 | ref = '2test'; 25 | assertTrue(ref.endsWith(test)); 26 | test = '3test'; 27 | assertFalse(ref.endsWith(test)); 28 | } 29 | 30 | function testEquals(string memory test) public { 31 | assertTrue(test.equals(test)); 32 | string memory modified = string.concat(test, 'a'); 33 | assertFalse(test.equals(modified)); 34 | } 35 | 36 | function testEndsWith(string memory suffix) public { 37 | string memory ref = string.concat('prefix', suffix); 38 | assertTrue(ref.endsWith(suffix)); 39 | } 40 | 41 | function testStartsWith(string memory prefix) public { 42 | string memory ref = string.concat(prefix, 'suffix'); 43 | assertTrue(ref.startsWith(prefix)); 44 | } 45 | 46 | function testFuzzContains(string memory test) public { 47 | string memory ref = string.concat('prefix', test, 'suffix'); 48 | assertTrue(ref.contains(test)); 49 | } 50 | 51 | // function testFuzzContainsFixedSuffix( 52 | // string memory test, 53 | // string memory prefix 54 | // ) public { 55 | // string memory ref = string.concat(prefix, test, 'suffix'); 56 | // assertTrue(ref.contains(test)); 57 | // } 58 | 59 | // function testFuzzContainsFixedPrefix( 60 | // string memory test, 61 | // string memory suffix 62 | // ) public { 63 | // string memory ref = string.concat('prefix', test, suffix); 64 | // assertTrue(ref.contains(test)); 65 | // } 66 | 67 | function testContainsString() public { 68 | string memory ref = 'prefixsuffix'; 69 | assertFalse(ref.contains('hello')); 70 | assertTrue(ref.contains('prefix')); 71 | assertTrue(ref.contains('suffix')); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/lib/BitMapUtility.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {BitMapUtility} from 'bound-layerable/lib/BitMapUtility.sol'; 6 | 7 | contract BitMapUtilityTest is Test { 8 | using BitMapUtility for uint256; 9 | using BitMapUtility for uint8; 10 | 11 | function testToBitMap(uint8 numBits) public { 12 | assertEq(numBits.toBitMap(), uint256(1 << numBits)); 13 | } 14 | 15 | function testIsSupersetOf(uint256 superset, uint256 subset) public { 16 | superset |= subset; 17 | assertTrue(superset.isSupersetOf(subset)); 18 | } 19 | 20 | function testIsSupersetOfNotSuperset(uint256 badSuperset, uint256 subset) 21 | public 22 | { 23 | badSuperset &= subset; 24 | if (badSuperset == subset) { 25 | if (subset != type(uint256).max) { 26 | subset += 1; 27 | } else { 28 | badSuperset = subset - 1; 29 | } 30 | } 31 | assertTrue(badSuperset != subset); 32 | assertFalse(badSuperset.isSupersetOf(subset)); 33 | } 34 | 35 | function testUnpackBitMap(uint8 numBits) public { 36 | uint256[] memory bits = new uint256[](numBits); 37 | for (uint8 i = 0; i < numBits; ++i) { 38 | bits[i] = i; 39 | } 40 | uint256 bitMap = BitMapUtility.uintsToBitMap(bits); 41 | 42 | if (numBits == 0) { 43 | assertEq(bitMap, 0); 44 | } else { 45 | assertEq(bitMap, (1 << numBits) - 1); 46 | } 47 | 48 | uint256[] memory unpacked = BitMapUtility.unpackBitMap(bitMap); 49 | assertEq(unpacked.length, numBits); 50 | 51 | for (uint8 i = 0; i < numBits; ++i) { 52 | assertEq(unpacked[i], i); 53 | } 54 | } 55 | 56 | function testUnpackBitMap1and255() public { 57 | uint256 bitMap = (1 << 255) | 1; 58 | uint256[] memory unpacked = BitMapUtility.unpackBitMap(bitMap); 59 | assertEq(unpacked.length, 2); 60 | assertEq(unpacked[0], 0); 61 | assertEq(unpacked[1], 255); 62 | } 63 | 64 | function testUnpackBitMap32Ones() public { 65 | uint256 bitMap = 2**32 - 1; 66 | uint256[] memory unpacked = BitMapUtility.unpackBitMap(bitMap); 67 | assertEq(unpacked.length, 32); 68 | for (uint8 i = 0; i < 32; ++i) { 69 | assertEq(unpacked[i], i); 70 | } 71 | } 72 | 73 | function testUnpackBitMapOopsAllOnes() public { 74 | uint256 bitMap = (1 << 255) - 1; 75 | uint256[] memory unpacked = BitMapUtility.unpackBitMap(bitMap); 76 | assertEq(unpacked.length, 255); 77 | for (uint8 i = 0; i < 255; ++i) { 78 | assertEq(unpacked[i], i); 79 | } 80 | } 81 | 82 | function testMsb(uint8 msb, uint256 extraBits) public { 83 | uint256 bitMask; 84 | if (msb == 255) { 85 | bitMask = 2**256 - 1; 86 | } else { 87 | // subtract 1 from 2**(msb+1) to get bitmask for all including and below msb 88 | bitMask = (1 << (msb + 1)) - 1; 89 | } 90 | 91 | uint256 bitMap = ((1 << msb) | extraBits) & bitMask; 92 | uint256 retrievedMsb = BitMapUtility.msb(bitMap); 93 | assertEq(retrievedMsb, msb); 94 | } 95 | 96 | function testLsbZero() public { 97 | assertEq(BitMapUtility.lsb(0), 0); 98 | } 99 | 100 | function testMsbZero() public { 101 | assertEq(BitMapUtility.msb(0), 0); 102 | } 103 | 104 | function testLsb(uint8 lsb, uint256 extraBits) public { 105 | vm.assume(!(lsb == 0 && extraBits == 0)); 106 | 107 | // set lsb to active 108 | // OR with extraBits 109 | // truncate bits below LSB by shifting and then shifting back 110 | uint256 bitMap = (((1 << lsb) | extraBits) >> lsb) << lsb; 111 | 112 | uint256 retrievedLsb = BitMapUtility.lsb(bitMap); 113 | assertEq(retrievedLsb, lsb); 114 | } 115 | 116 | function test_fuzzLsb(uint256 randomBits) public pure { 117 | BitMapUtility.lsb(randomBits); 118 | } 119 | 120 | function test_fuzzMsb(uint256 randomBits) public pure { 121 | BitMapUtility.msb(randomBits); 122 | } 123 | 124 | function testContains(uint8 byteVal) public { 125 | assertTrue(byteVal.toBitMap().contains(byteVal)); 126 | } 127 | 128 | function testUintsToBitMap(uint8[256] memory bits) public { 129 | uint256[] memory castBits = new uint256[](bits.length); 130 | for (uint256 i = 0; i < bits.length; ++i) { 131 | castBits[i] = bits[i]; 132 | } 133 | 134 | uint256 bitMap = BitMapUtility.uintsToBitMap(castBits); 135 | 136 | for (uint256 i = 0; i < bits.length; ++i) { 137 | assertTrue(bitMap.contains(bits[i])); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /test/lib/JSON.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {json} from 'bound-layerable/lib/JSON.sol'; 6 | import {StringTestUtility} from '../helpers/StringTestUtility.sol'; 7 | 8 | contract JsonTest is Test { 9 | using json for string; 10 | using StringTestUtility for string; 11 | using StringTestUtility for string[]; 12 | 13 | function testObject(string memory objectContents) public { 14 | string memory objectified = objectContents.object(); 15 | assertTrue(objectified.startsWith('{')); 16 | assertTrue(objectified.endsWith('}')); 17 | } 18 | 19 | function testArray(string memory arrayContents) public { 20 | string memory arrayified = arrayContents.array(); 21 | assertTrue(arrayified.startsWith('[')); 22 | assertTrue(arrayified.endsWith(']')); 23 | } 24 | 25 | function testProperty(string memory name, string memory value) public { 26 | string memory propertyified = json.property(name, value); 27 | assertTrue(propertyified.startsWith('"')); 28 | assertTrue(propertyified.endsWith('"')); 29 | assertTrue(propertyified.contains(bytes1(':'))); 30 | assertTrue(propertyified.startsWith(name.quote())); 31 | assertTrue(propertyified.endsWith(value.quote())); 32 | } 33 | 34 | function testRawProperty(string memory name, string memory value) public { 35 | string memory propertyified = json.rawProperty(name, value); 36 | assertTrue(propertyified.startsWith('"')); 37 | if (!value.endsWith('"')) { 38 | assertFalse(propertyified.endsWith('"')); 39 | } 40 | assertTrue(propertyified.contains(bytes1(':'))); 41 | assertTrue(propertyified.startsWith(name.quote())); 42 | assertTrue(propertyified.endsWith(value)); 43 | } 44 | 45 | function testObjectOf( 46 | string memory name, 47 | string memory value, 48 | uint8 num 49 | ) public { 50 | string memory property = num > 0 ? json.property(name, value) : ''; 51 | string[] memory properties = new string[](num); 52 | for (uint8 i = 0; i < num; i++) { 53 | properties[i] = property; 54 | } 55 | string memory objectified = json.objectOf(properties); 56 | assertTrue(objectified.startsWith('{')); 57 | assertTrue(objectified.endsWith('}')); 58 | assertTrue(objectified.startsWith(string.concat('{', property))); 59 | assertTrue(objectified.endsWith(string.concat(property, '}'))); 60 | uint256 countNativeComma = property.countChar(','); 61 | uint256 expectedAddedCommas = num > 0 ? num - 1 : 0; 62 | uint256 expectedNativeCommas = num * countNativeComma; 63 | assertEq( 64 | objectified.countChar(','), 65 | expectedAddedCommas + expectedNativeCommas 66 | ); 67 | } 68 | 69 | function testArrayOf(string memory value, uint8 num) public { 70 | value = num > 0 ? value : ''; 71 | string[] memory values = new string[](num); 72 | for (uint8 i = 0; i < num; i++) { 73 | values[i] = value; 74 | } 75 | string memory jsonArray = json.arrayOf(values); 76 | assertTrue(jsonArray.startsWith('[')); 77 | assertTrue(jsonArray.endsWith(']')); 78 | assertTrue(jsonArray.startsWith(string.concat('[', value))); 79 | assertTrue(jsonArray.endsWith(string.concat(value, ']'))); 80 | uint256 countNativeComma = value.countChar(','); 81 | uint256 expectedAddedCommas = num > 0 ? num - 1 : 0; 82 | uint256 expectedNativeCommas = num * countNativeComma; 83 | assertEq( 84 | jsonArray.countChar(','), 85 | expectedAddedCommas + expectedNativeCommas 86 | ); 87 | } 88 | 89 | function testArrayOfTwo( 90 | string memory value, 91 | uint8 num1, 92 | uint8 num2 93 | ) public { 94 | num1 = uint8(bound(num1, 0, 127)); 95 | num2 = uint8(bound(num2, 0, 127)); 96 | uint256 total = num1 + num2; 97 | value = total > 0 ? value : ''; 98 | string[] memory values1 = new string[](num1); 99 | for (uint8 i = 0; i < num1; i++) { 100 | values1[i] = value; 101 | } 102 | string[] memory values2 = new string[](num2); 103 | for (uint8 i = 0; i < num2; i++) { 104 | values2[i] = value; 105 | } 106 | string memory jsonArray = json.arrayOf(values1, values2); 107 | emit log_named_string('jsonArray', jsonArray); 108 | assertTrue(jsonArray.startsWith('[')); 109 | assertTrue(jsonArray.endsWith(']')); 110 | assertTrue(jsonArray.startsWith(string.concat('[', value))); 111 | assertTrue(jsonArray.endsWith(string.concat(value, ']'))); 112 | uint256 countNativeComma = value.countChar(','); 113 | uint256 expectedAddedCommas = total > 0 ? total - 1 : 0; 114 | uint256 expectedNativeCommas = total * countNativeComma; 115 | assertEq( 116 | jsonArray.countChar(','), 117 | expectedAddedCommas + expectedNativeCommas 118 | ); 119 | } 120 | 121 | function testQuote(string memory value) public { 122 | string memory quoted = value.quote(); 123 | assertTrue(quoted.startsWith('"')); 124 | assertTrue(quoted.endsWith('"')); 125 | assertTrue(quoted.contains(value)); 126 | } 127 | 128 | function testJoinComma(string memory str, uint8 times) public { 129 | times = uint8(bound(times, 1, 255)); 130 | string[] memory strings = new string[](times); 131 | for (uint8 i = 0; i < times; i++) { 132 | strings[i] = str; 133 | } 134 | string memory joined = json._commaJoin(strings); 135 | assertTrue(joined.startsWith(str)); 136 | assertTrue(joined.endsWith(str)); 137 | uint256 countNativeComma = str.countChar(','); 138 | uint256 expectedAddedCommas = times - 1; 139 | uint256 expectedNativeCommas = times * countNativeComma; 140 | assertEq( 141 | joined.countChar(','), 142 | expectedAddedCommas + expectedNativeCommas 143 | ); 144 | } 145 | 146 | function testJoinComma() public { 147 | string memory a = 'a'; 148 | string memory b = 'b'; 149 | string memory joined = 'a,b'; 150 | assertEq(json._commaJoin(a, b), joined); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /test/metadata/Layerable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {ImageLayerable} from 'bound-layerable/metadata/ImageLayerable.sol'; 6 | import {Attribute} from 'bound-layerable/interface/Structs.sol'; 7 | import {DisplayType, LayerType} from 'bound-layerable/interface/Enums.sol'; 8 | import {PackedByteUtility} from 'bound-layerable/lib/PackedByteUtility.sol'; 9 | import {BitMapUtility} from 'bound-layerable/lib/BitMapUtility.sol'; 10 | import {StringTestUtility} from '../helpers/StringTestUtility.sol'; 11 | import {LibString} from 'solady/utils/LibString.sol'; 12 | import {InvalidInitialization} from 'bound-layerable/interface/Errors.sol'; 13 | 14 | contract LayerableImpl is ImageLayerable { 15 | uint256 bindings; 16 | uint256[] activeLayers; 17 | bytes32 packedBatchRandomness; 18 | 19 | constructor() 20 | ImageLayerable( 21 | msg.sender, 22 | 'default', 23 | 100, 24 | 100, 25 | 'external', 26 | 'description' 27 | ) 28 | {} 29 | 30 | function setBindings(uint256 _bindings) public { 31 | bindings = _bindings; 32 | } 33 | 34 | function setActiveLayers(uint256[] memory _activeLayers) public { 35 | activeLayers = _activeLayers; 36 | } 37 | 38 | function setPackedBatchRandomness(bytes32 _packedBatchRandomness) public { 39 | packedBatchRandomness = _packedBatchRandomness; 40 | } 41 | 42 | function tokenURI(uint256 layerId) 43 | public 44 | view 45 | virtual 46 | returns (string memory) 47 | { 48 | return 49 | this.getTokenURI( 50 | layerId, 51 | layerId, 52 | bindings, 53 | activeLayers, 54 | packedBatchRandomness 55 | ); 56 | } 57 | } 58 | 59 | contract LayerableTest is Test { 60 | using BitMapUtility for uint256; 61 | using StringTestUtility for string; 62 | using LibString for uint256; 63 | using LibString for uint8; 64 | 65 | LayerableImpl test; 66 | 67 | event OwnershipTransferred( 68 | address indexed previousOwner, 69 | address indexed newOwner 70 | ); 71 | 72 | function setUp() public { 73 | test = new LayerableImpl(); 74 | test.setBaseLayerURI('layer/'); // test.setLayerTypeDistribution(LayerType.PORTRAIT, 0xFF << 248); 75 | } 76 | 77 | function testInitialize() public { 78 | vm.expectEmit(true, true, false, false); 79 | emit OwnershipTransferred(address(0), address(this)); 80 | test = new LayerableImpl(); 81 | vm.expectRevert(InvalidInitialization.selector); 82 | test.initialize(address(this)); 83 | } 84 | 85 | function testGetActiveLayerTraits(uint8[2] memory activeLayers) public { 86 | uint256[] memory activeLayersCopy = new uint256[](2); 87 | for (uint8 i = 0; i < activeLayers.length; i++) { 88 | activeLayersCopy[i] = activeLayers[i]; 89 | } 90 | for (uint256 i = 0; i < activeLayers.length; i++) { 91 | test.setAttribute( 92 | activeLayers[i], 93 | Attribute( 94 | activeLayers[i].toString(), 95 | activeLayers[i].toString(), 96 | DisplayType.String 97 | ) 98 | ); 99 | } 100 | 101 | string memory actual = test.getActiveLayerTraits(activeLayersCopy); 102 | 103 | emit log_string(actual); 104 | for (uint256 i = 0; i < activeLayers.length; i++) { 105 | assertTrue( 106 | actual.contains( 107 | string.concat( 108 | '{"trait_type":"Active ', 109 | activeLayers[i].toString(), 110 | '","value":"', 111 | activeLayers[i].toString(), 112 | '"}' 113 | ) 114 | ) 115 | ); 116 | } 117 | } 118 | 119 | function testBoundLayerTraits(uint8[2] memory boundLayers) public { 120 | uint256 bindings; 121 | for (uint256 i = 0; i < boundLayers.length; i++) { 122 | bindings |= 1 << boundLayers[i]; 123 | 124 | test.setAttribute( 125 | boundLayers[i], 126 | Attribute( 127 | boundLayers[i].toString(), 128 | boundLayers[i].toString(), 129 | DisplayType.String 130 | ) 131 | ); 132 | } 133 | 134 | string memory actual = test.getBoundLayerTraits(bindings); 135 | 136 | emit log_string(actual); 137 | for (uint256 i = 0; i < boundLayers.length; i++) { 138 | assertTrue( 139 | actual.contains( 140 | string.concat( 141 | '{"trait_type":"', 142 | boundLayers[i].toString(), 143 | '","value":"', 144 | boundLayers[i].toString(), 145 | '"}' 146 | ) 147 | ) 148 | ); 149 | } 150 | } 151 | 152 | function testInitialize_InvalidInitialization() public { 153 | vm.expectRevert(abi.encodeWithSelector(InvalidInitialization.selector)); 154 | test.initialize(address(0)); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /test/traits/OnChainMultiTraits.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {OnChainMultiTraits} from 'bound-layerable/traits/OnChainMultiTraits.sol'; 6 | import {Attribute} from 'bound-layerable/interface/Structs.sol'; 7 | import {DisplayType} from 'bound-layerable/interface/Enums.sol'; 8 | 9 | // concrete implementation 10 | contract OnChainTraitsImpl is OnChainMultiTraits { 11 | 12 | } 13 | 14 | contract OnChainMultiTraitsTest is Test { 15 | OnChainMultiTraits test; 16 | 17 | function setUp() public { 18 | test = new OnChainTraitsImpl(); 19 | } 20 | 21 | function testGetLayerTraitJson() public { 22 | Attribute memory attribute = Attribute( 23 | 'test', 24 | 'hello', 25 | DisplayType.String 26 | ); 27 | Attribute[] memory attributes = new Attribute[](1); 28 | attributes[0] = attribute; 29 | test.setAttribute(1, attributes); 30 | string memory expected = '{"trait_type":"test","value":"hello"}'; 31 | string memory actual = test.getLayerTraitJson(1); 32 | assertEq(abi.encode(actual), abi.encode(expected)); 33 | 34 | attribute.displayType = DisplayType.Date; 35 | test.setAttribute(2, attributes); 36 | expected = '{"trait_type":"test","display_type":"date","value":"hello"}'; 37 | actual = test.getLayerTraitJson(2); 38 | assertEq(abi.encode(actual), abi.encode(expected)); 39 | 40 | expected = '{"trait_type":"qual test","display_type":"date","value":"hello"}'; 41 | actual = test.getLayerTraitJson(2, 'qual'); 42 | assertEq(abi.encode(actual), abi.encode(expected)); 43 | } 44 | 45 | function testSetAttributes() public { 46 | uint256[] memory traitIds = new uint256[](2); 47 | traitIds[0] = 1; 48 | traitIds[1] = 2; 49 | Attribute[][] memory attributes = new Attribute[][](2); 50 | attributes[0] = new Attribute[](1); 51 | attributes[1] = new Attribute[](1); 52 | attributes[0][0] = Attribute('test', 'hello', DisplayType.String); 53 | attributes[1][0] = Attribute('test', 'hello2', DisplayType.String); 54 | test.setAttributes(traitIds, attributes); 55 | 56 | string memory expected = '{"trait_type":"test","value":"hello"}'; 57 | string memory actual = test.getLayerTraitJson(1); 58 | assertEq(bytes(actual), bytes(expected)); 59 | 60 | expected = '{"trait_type":"test","value":"hello2"}'; 61 | actual = test.getLayerTraitJson(2); 62 | assertEq(bytes(actual), bytes(expected)); 63 | } 64 | 65 | function testSetAttribute_onlyOwner(address addr) public { 66 | vm.assume(addr != address(this)); 67 | Attribute[] memory attribute = new Attribute[](1); 68 | attribute[0] = Attribute('test', 'hello', DisplayType.String); 69 | test.setAttribute(1, attribute); 70 | vm.startPrank(addr); 71 | vm.expectRevert(0x5fc483c5); 72 | test.setAttribute(1, attribute); 73 | } 74 | 75 | function testSetAttributes_onlyOwner(address addr) public { 76 | vm.assume(addr != address(this)); 77 | uint256[] memory traitIds = new uint256[](2); 78 | traitIds[0] = 1; 79 | traitIds[1] = 2; 80 | Attribute[][] memory attributes = new Attribute[][](2); 81 | attributes[0] = new Attribute[](1); 82 | attributes[1] = new Attribute[](1); 83 | attributes[0][0] = Attribute('test', 'hello', DisplayType.String); 84 | attributes[1][0] = Attribute('test', 'hello2', DisplayType.String); 85 | 86 | test.setAttributes(traitIds, attributes); 87 | vm.startPrank(addr); 88 | vm.expectRevert(0x5fc483c5); 89 | test.setAttributes(traitIds, attributes); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/traits/OnChainTraits.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {OnChainTraits} from 'bound-layerable/traits/OnChainTraits.sol'; 6 | import {Attribute} from 'bound-layerable/interface/Structs.sol'; 7 | import {DisplayType} from 'bound-layerable/interface/Enums.sol'; 8 | import {ArrayLengthMismatch} from 'bound-layerable/interface/Errors.sol'; 9 | 10 | // concrete implementation 11 | contract OnChainTraitsImpl is OnChainTraits { 12 | function getAttributeJson( 13 | string memory properties, 14 | Attribute memory attribute 15 | ) public pure returns (string memory) { 16 | return _getAttributeJson(properties, attribute); 17 | } 18 | } 19 | 20 | contract OnChainTraitsTest is Test { 21 | OnChainTraitsImpl test; 22 | 23 | function setUp() public { 24 | test = new OnChainTraitsImpl(); 25 | } 26 | 27 | function testGetLayerTraitJson() public { 28 | test.setAttribute(1, Attribute('test', 'hello', DisplayType.String)); 29 | string memory expected = '{"trait_type":"test","value":"hello"}'; 30 | string memory actual = test.getLayerTraitJson(1); 31 | assertEq(abi.encode(actual), abi.encode(expected)); 32 | 33 | test.setAttribute(2, Attribute('test', 'hello', DisplayType.Date)); 34 | expected = '{"trait_type":"test","display_type":"date","value":"hello"}'; 35 | actual = test.getLayerTraitJson(2); 36 | assertEq(abi.encode(actual), abi.encode(expected)); 37 | 38 | expected = '{"trait_type":"qual test","display_type":"date","value":"hello"}'; 39 | actual = test.getLayerTraitJson(2, 'qual'); 40 | assertEq(abi.encode(actual), abi.encode(expected)); 41 | 42 | test.setAttribute(2, Attribute('test', 'hello', DisplayType.Number)); 43 | expected = '{"trait_type":"test","display_type":"number","value":"hello"}'; 44 | actual = test.getLayerTraitJson(2); 45 | assertEq(abi.encode(actual), abi.encode(expected)); 46 | 47 | test.setAttribute( 48 | 2, 49 | Attribute('test', 'hello', DisplayType.BoostPercent) 50 | ); 51 | expected = '{"trait_type":"test","display_type":"boost_percent","value":"hello"}'; 52 | actual = test.getLayerTraitJson(2); 53 | assertEq(abi.encode(actual), abi.encode(expected)); 54 | 55 | test.setAttribute( 56 | 2, 57 | Attribute('test', 'hello', DisplayType.BoostNumber) 58 | ); 59 | expected = '{"trait_type":"test","display_type":"boost_number","value":"hello"}'; 60 | actual = test.getLayerTraitJson(2); 61 | assertEq(abi.encode(actual), abi.encode(expected)); 62 | } 63 | 64 | function testSetAttributes() public { 65 | uint256[] memory traitIds = new uint256[](2); 66 | traitIds[0] = 1; 67 | traitIds[1] = 2; 68 | Attribute[] memory attributes = new Attribute[](2); 69 | attributes[0] = Attribute('test', 'hello', DisplayType.String); 70 | attributes[1] = Attribute('test', 'hello2', DisplayType.String); 71 | test.setAttributes(traitIds, attributes); 72 | 73 | string memory expected = '{"trait_type":"test","value":"hello"}'; 74 | string memory actual = test.getLayerTraitJson(1); 75 | assertEq(bytes(actual), bytes(expected)); 76 | 77 | expected = '{"trait_type":"test","value":"hello2"}'; 78 | actual = test.getLayerTraitJson(2); 79 | assertEq(bytes(actual), bytes(expected)); 80 | } 81 | 82 | function testSetAttributes_mismatch() public { 83 | uint256[] memory traitIds = new uint256[](2); 84 | traitIds[0] = 1; 85 | traitIds[1] = 2; 86 | Attribute[] memory attributes = new Attribute[](1); 87 | attributes[0] = Attribute('test', 'hello', DisplayType.String); 88 | vm.expectRevert( 89 | abi.encodeWithSelector(ArrayLengthMismatch.selector, 2, 1) 90 | ); 91 | test.setAttributes(traitIds, attributes); 92 | } 93 | 94 | function testSetAttribute_onlyOwner(address addr) public { 95 | vm.assume(addr != address(this)); 96 | test.setAttribute(1, Attribute('test', 'hello', DisplayType.String)); 97 | vm.startPrank(addr); 98 | vm.expectRevert(0x5fc483c5); 99 | test.setAttribute(1, Attribute('test', 'hello', DisplayType.String)); 100 | } 101 | 102 | function testSetAttributes_onlyOwner(address addr) public { 103 | vm.assume(addr != address(this)); 104 | uint256[] memory traitIds = new uint256[](2); 105 | traitIds[0] = 1; 106 | traitIds[1] = 2; 107 | Attribute[] memory attributes = new Attribute[](2); 108 | attributes[0] = Attribute('test', 'hello', DisplayType.String); 109 | attributes[1] = Attribute('test', 'hello2', DisplayType.String); 110 | 111 | test.setAttributes(traitIds, attributes); 112 | vm.startPrank(addr); 113 | vm.expectRevert(0x5fc483c5); 114 | test.setAttributes(traitIds, attributes); 115 | } 116 | 117 | function testGetAttributeJson() public { 118 | Attribute memory attribute = Attribute( 119 | 'test', 120 | 'hello', 121 | DisplayType.String 122 | ); 123 | string memory expected = '{"value":"hello"}'; 124 | string memory actual = test.getAttributeJson('', attribute); 125 | assertEq(actual, expected); 126 | attribute.displayType = DisplayType.Date; 127 | expected = '{"display_type":"date","value":"hello"}'; 128 | actual = test.getAttributeJson('', attribute); 129 | assertEq(actual, expected); 130 | attribute.displayType = DisplayType.Number; 131 | expected = '{"display_type":"number","value":"hello"}'; 132 | actual = test.getAttributeJson('', attribute); 133 | assertEq(actual, expected); 134 | attribute.displayType = DisplayType.BoostPercent; 135 | expected = '{"display_type":"boost_percent","value":"hello"}'; 136 | actual = test.getAttributeJson('', attribute); 137 | assertEq(actual, expected); 138 | attribute.displayType = DisplayType.BoostNumber; 139 | expected = '{"display_type":"boost_number","value":"hello"}'; 140 | actual = test.getAttributeJson('', attribute); 141 | assertEq(actual, expected); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /test/traits/RandomTraits.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.4; 3 | 4 | import {Test} from 'forge-std/Test.sol'; 5 | import {RandomTraits} from 'bound-layerable/traits/RandomTraits.sol'; 6 | import {PackedByteUtility} from 'bound-layerable/lib/PackedByteUtility.sol'; 7 | import {LayerType} from 'bound-layerable/interface/Enums.sol'; 8 | import {RandomTraitsImpl} from 'bound-layerable/traits/RandomTraitsImpl.sol'; 9 | import {BadDistributions, InvalidLayerType, ArrayLengthMismatch} from 'bound-layerable/interface/Errors.sol'; 10 | 11 | contract RandomTraitsTestImpl is RandomTraitsImpl { 12 | constructor(uint8 numTokensPerSet) 13 | RandomTraits( 14 | '', 15 | '', 16 | address(1234), 17 | 5555, 18 | numTokensPerSet, 19 | 1, 20 | 16, 21 | bytes32(uint256(1)) 22 | ) 23 | {} 24 | 25 | function setPackedBatchRandomness(bytes32 seed) public { 26 | packedBatchRandomness = seed; 27 | } 28 | 29 | function getLayerTypeDistributions(uint8 layerType) 30 | public 31 | view 32 | returns (uint256[2] memory) 33 | { 34 | return layerTypeToPackedDistributions[layerType]; 35 | } 36 | 37 | function getLayerSeedPub( 38 | uint256 tokenId, 39 | uint8 layerType, 40 | bytes32 seed 41 | ) public pure returns (uint16) { 42 | return getLayerSeed(tokenId, layerType, seed); 43 | } 44 | 45 | uint256[2] _distributions; 46 | 47 | function getLayerIdPub( 48 | uint8 layerType, 49 | uint256 layerSeed, 50 | uint256[2] memory distributions 51 | ) public returns (uint256) { 52 | _distributions = distributions; 53 | return getLayerId(layerType, layerSeed, _distributions); 54 | } 55 | } 56 | 57 | contract RandomTraitsTest is Test { 58 | RandomTraitsTestImpl test; 59 | uint256[] distributions; 60 | 61 | function setUp() public { 62 | test = new RandomTraitsTestImpl(7); 63 | } 64 | 65 | function testSetLayerTypeDistributions() public { 66 | uint8[] memory layerTypes = new uint8[](8); 67 | uint256[2][] memory dists = new uint256[2][](8); 68 | for (uint256 i; i < 8; ++i) { 69 | layerTypes[i] = uint8(i); 70 | dists[i][0] = i + 1; 71 | } 72 | test.setLayerTypeDistributions(layerTypes, dists); 73 | for (uint256 i; i < 8; ++i) { 74 | assertEq( 75 | keccak256( 76 | abi.encode(test.getLayerTypeDistributions(layerTypes[i])) 77 | ), 78 | keccak256(abi.encode(dists[i])) 79 | ); 80 | } 81 | 82 | layerTypes = new uint8[](1); 83 | 84 | vm.expectRevert( 85 | abi.encodeWithSelector(ArrayLengthMismatch.selector, 1, 8) 86 | ); 87 | test.setLayerTypeDistributions(layerTypes, dists); 88 | } 89 | 90 | function testSetLayerTypeDistribution( 91 | uint8 layerType, 92 | uint256[2] memory distribution 93 | ) public { 94 | layerType = uint8(bound(layerType, 0, 7)); 95 | test.setLayerTypeDistribution(layerType, distribution); 96 | assertEq( 97 | keccak256(abi.encode(test.getLayerTypeDistributions(layerType))), 98 | keccak256(abi.encode(distribution)) 99 | ); 100 | } 101 | 102 | function testSetLayerTypeDistributionInvalidLayerType(uint8 layerType) 103 | public 104 | { 105 | layerType = uint8(bound(layerType, 8, 255)); 106 | vm.expectRevert(abi.encodeWithSelector(InvalidLayerType.selector)); 107 | uint256[2] memory _distributions = [uint256(0), uint256(0)]; 108 | test.setLayerTypeDistribution(layerType, _distributions); 109 | } 110 | 111 | function testSetLayerTypeDistributionNotOwner(address notOwner) public { 112 | vm.assume(notOwner != address(this)); 113 | vm.startPrank(notOwner); 114 | uint256[2] memory _distributions = [uint256(1), uint256(0)]; 115 | 116 | vm.expectRevert(0x5fc483c5); 117 | test.setLayerTypeDistribution(0, _distributions); 118 | } 119 | 120 | function testGetLayerSeedShifts() public { 121 | uint256 validTokenId = 1; 122 | // test that we are correctly packing values by providing a tokenId that will be truncated to 248 bits 123 | uint256 truncatedTokenId = 2**248 + 1; 124 | bytes32 seed = bytes32(uint256(1)); 125 | bytes32 seed2 = bytes32(uint256(2)); 126 | uint8 layerType = 1; 127 | uint8 layerType2 = 2; 128 | 129 | assertEq( 130 | test.getLayerSeedPub(truncatedTokenId, layerType, seed), 131 | test.getLayerSeedPub(validTokenId, layerType, seed) 132 | ); 133 | assertFalse( 134 | test.getLayerSeedPub(truncatedTokenId, layerType, seed) == 135 | test.getLayerSeedPub(validTokenId, layerType, seed2) 136 | ); 137 | assertFalse( 138 | test.getLayerSeedPub(truncatedTokenId, layerType, seed) == 139 | test.getLayerSeedPub(validTokenId, layerType2, seed) 140 | ); 141 | } 142 | 143 | function testGetLayerIdBounds(uint256 packedBatchRandomness) public { 144 | packedBatchRandomness = bound( 145 | packedBatchRandomness, 146 | 1, 147 | (1 << test.BITS_PER_RANDOM_BATCH()) - 1 148 | ); 149 | // vm.assume(packedBatchRandomness != 0); 150 | test.setPackedBatchRandomness(bytes32(uint256(packedBatchRandomness))); 151 | distributions.push(0x80); 152 | test.setLayerTypeDistribution( 153 | uint8(LayerType.PORTRAIT), 154 | PackedByteUtility.packArrayOfShorts(distributions) 155 | ); 156 | uint256 layerId = test.getLayerId(0); 157 | assertTrue(layerId == 1 || layerId == 2); 158 | } 159 | 160 | function testGetLayerIdBounds( 161 | uint256 packedBatchRandomness, 162 | uint8 numDistributions 163 | ) public { 164 | packedBatchRandomness = bound( 165 | packedBatchRandomness, 166 | 1, 167 | (1 << test.BITS_PER_RANDOM_BATCH()) - 1 168 | ); 169 | test.setPackedBatchRandomness(bytes32(uint256(packedBatchRandomness))); 170 | 171 | numDistributions = uint8(bound(numDistributions, 1, 32)); 172 | for (uint256 i = 0; i < numDistributions; ++i) { 173 | // ~ evenly split distributions 174 | uint256 num = (i + 1) * 8; 175 | if (num == 256) { 176 | num == 255; 177 | } 178 | distributions.push(num); 179 | } 180 | test.setLayerTypeDistribution( 181 | uint8(LayerType.PORTRAIT), 182 | PackedByteUtility.packArrayOfShorts(distributions) 183 | ); 184 | uint256 layerId = test.getLayerId(0); 185 | assertGt(layerId, 0); 186 | assertLt(layerId, 33); 187 | } 188 | 189 | function testGetLayerIdBoundsDirect( 190 | uint256 layerSeed, 191 | uint8 layerType, 192 | uint8 numDistributions, 193 | uint16 increment 194 | ) public { 195 | layerSeed = bound( 196 | layerSeed, 197 | 1, 198 | (1 << test.BITS_PER_RANDOM_BATCH()) - 1 199 | ); 200 | layerType = uint8(bound(layerType, 0, 7)); 201 | numDistributions = uint8(bound(numDistributions, 1, 32)); 202 | increment = uint16(bound(increment, 1, 2048)); 203 | 204 | for (uint256 i = 0; i < numDistributions; ++i) { 205 | // ~ evenly split distributions 206 | uint256 num = (i + 1) * increment; 207 | if (num == 65536) { 208 | num == 65535; 209 | } 210 | distributions.push(num); 211 | } 212 | uint256[2] memory distributionPacked = PackedByteUtility 213 | .packArrayOfShorts(distributions); 214 | emit log_named_uint('distributions[0]', distributionPacked[0]); 215 | emit log_named_uint('distributions[1]', distributionPacked[1]); 216 | 217 | // test will revert if it's the last layer type and ends at an index higher than 31 218 | // since it will try to assign layerId to 256 219 | bool willRevert = layerType == 7 && 220 | numDistributions > 30 && 221 | // if gte this value, will be assigned to index 32 and overflow 222 | layerSeed >= 31 * uint256(increment); 223 | uint256 layerId; 224 | if (willRevert) { 225 | vm.expectRevert(abi.encodeWithSelector(BadDistributions.selector)); 226 | test.getLayerIdPub( 227 | layerType, 228 | uint256(layerSeed), 229 | distributionPacked 230 | ); 231 | } else { 232 | layerId = test.getLayerIdPub( 233 | layerType, 234 | uint256(layerSeed), 235 | distributionPacked 236 | ); 237 | 238 | uint256 layerTypeOffset = uint256(layerType) * 32; 239 | assertGt(layerId, 0 + layerTypeOffset); 240 | assertLt(layerId, 33 + layerTypeOffset); 241 | assertLt(layerId, 256); 242 | 243 | uint256 bin = uint256(layerSeed) / uint256(increment) + 1; 244 | if (bin > numDistributions) { 245 | if (numDistributions == 32) { 246 | bin = 32; 247 | } else { 248 | bin = numDistributions + 1; 249 | } 250 | } 251 | assertEq(layerId, bin + layerTypeOffset); 252 | } 253 | } 254 | 255 | function testGetLayerType() public { 256 | distributions = new uint256[](0); 257 | // % 7 == 0 should be portrait 258 | assertEq(uint256(test.getLayerType(0)), 0); 259 | assertEq(uint256(test.getLayerType(7)), 0); 260 | 261 | // % 7 == 1 should be background 262 | assertEq(uint256(test.getLayerType(1)), 1); 263 | assertEq(uint256(test.getLayerType(8)), 1); 264 | 265 | // % 7 == 2 should be texture 266 | assertEq(uint256(test.getLayerType(2)), 2); 267 | assertEq(uint256(test.getLayerType(9)), 2); 268 | 269 | // % 7 == 3 should be object 270 | assertEq(uint256(test.getLayerType(3)), 3); 271 | assertEq(uint256(test.getLayerType(10)), 3); 272 | 273 | // % 7 == 4 should be object2 274 | assertEq(uint256(test.getLayerType(4)), 4); 275 | assertEq(uint256(test.getLayerType(11)), 4); 276 | 277 | // % 7 == 5 should be border 278 | assertEq(uint256(test.getLayerType(5)), 5); 279 | assertEq(uint256(test.getLayerType(12)), 5); 280 | 281 | // % 7 == 6 should be border 282 | assertEq(uint256(test.getLayerType(6)), 5); 283 | assertEq(uint256(test.getLayerType(13)), 5); 284 | } 285 | 286 | function testGetLayerId_NoDistributions() public { 287 | uint8 layerType = 0; 288 | uint256 layerSeed = 5; 289 | uint256[2] memory dists = [uint256(0), uint256(0)]; 290 | vm.expectRevert(BadDistributions.selector); 291 | test.getLayerIdPub(layerType, layerSeed, dists); 292 | } 293 | 294 | function testGetLayerId_badDistribution_layerType7_index31() public { 295 | uint8 layerType = 7; 296 | uint256 layerSeed = type(uint256).max; 297 | uint256[2] memory dists = [layerSeed, layerSeed]; 298 | dists[1] = dists[1] & (dists[1] << 16); 299 | vm.expectRevert(BadDistributions.selector); 300 | test.getLayerIdPub(layerType, layerSeed, dists); 301 | } 302 | 303 | function testGetLayerId_badDistribution_layerType7_index32() public { 304 | uint8 layerType = 7; 305 | uint256 layerSeed = type(uint256).max; 306 | uint256[2] memory dists = [layerSeed, layerSeed]; 307 | vm.expectRevert(BadDistributions.selector); 308 | test.getLayerIdPub(layerType, layerSeed, dists); 309 | } 310 | 311 | function testGetLayerId_badDistribution_layerType6_index31() public { 312 | uint8 layerType = 6; 313 | uint256 layerSeed = type(uint256).max; 314 | uint256[2] memory dists = [layerSeed, layerSeed]; 315 | dists[1] = dists[1] & (dists[1] << 16); 316 | uint256 id = test.getLayerIdPub(layerType, layerSeed, dists); 317 | assertEq(id, 6 * 32 + 32); 318 | } 319 | 320 | function testGetLayerId_badDistribution_layerType6_index32() public { 321 | uint8 layerType = 6; 322 | uint256 layerSeed = type(uint256).max; 323 | uint256[2] memory dists = [layerSeed, layerSeed]; 324 | // vm.expectRevert(BadDistributions.selector); 325 | uint256 id = test.getLayerIdPub(layerType, layerSeed, dists); 326 | assertEq(id, 6 * 32 + 32); 327 | } 328 | 329 | function testGetLayerId( 330 | uint8 layerType, 331 | uint8 index, 332 | uint8 numLayers 333 | ) public { 334 | // bound layertype 335 | layerType = uint8(bound(layerType, 0, 7)); 336 | // max is 31 if layerType is 7 337 | uint256 maxLayer = layerType == 7 ? 31 : 32; 338 | // bound numLayers 339 | numLayers = uint8(bound(numLayers, 1, maxLayer)); 340 | // bound index to numLayers (0-indexed) 341 | index = uint8(bound(index, 0, numLayers - 1)); 342 | 343 | // create distributions of sequential ints starting at 1 344 | uint256[2] memory dists = [uint256(0), uint256(0)]; 345 | for (uint256 i; i < numLayers; ++i) { 346 | // index within packed shorts 347 | uint256 j = i % 16; 348 | // which packed shorts to index 349 | uint256 k = i / 16; 350 | uint256 dist = dists[k]; 351 | // overwrite 352 | dists[k] = PackedByteUtility.packShortAtIndex(dist, i + 1, j); 353 | } 354 | // use index as seed 355 | uint256 layerId = test.getLayerIdPub(layerType, index, dists); 356 | assertEq(layerId, index + 1 + 32 * layerType); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /test/util/ERC721Recipient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity ^0.8.12; 3 | 4 | import {ERC721TokenReceiver} from 'solmate/tokens/ERC721.sol'; 5 | 6 | contract ERC721Recipient is ERC721TokenReceiver { 7 | function onERC721Received( 8 | address, 9 | address, 10 | uint256, 11 | bytes calldata 12 | ) public virtual override returns (bytes4) { 13 | return ERC721TokenReceiver.onERC721Received.selector; 14 | } 15 | } 16 | --------------------------------------------------------------------------------