├── .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 | '',
153 | _tag,
154 | '>'
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 | ''
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 |
--------------------------------------------------------------------------------