├── .gas-snapshot
├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── .gitmodules
├── .prettierignore
├── .prettierrc
├── .solhint.json
├── .vscode
└── settings.json
├── CONTRIBUTORS.md
├── LICENSE
├── README.md
├── analysis
├── README.md
├── python
│ ├── compute_price.py
│ └── pricer.py
├── requirements.txt
└── smt
│ └── goo_pooling.smt2
├── assets
├── gobbler.png
└── state-machines
│ ├── gobbler-lifecycle.png
│ ├── legendary-gobbler-auctions.png
│ └── page-auctions.png
├── foundry.toml
├── package-lock.json
├── package.json
├── remappings.txt
├── script
└── deploy
│ ├── DeployBase.s.sol
│ ├── DeployGoerli.s.sol
│ ├── DeployMainnet.s.sol
│ └── DeployRinkeby.s.sol
├── src
├── ArtGobblers.sol
├── Goo.sol
├── Pages.sol
└── utils
│ ├── GobblerReserve.sol
│ ├── rand
│ ├── ChainlinkV1RandProvider.sol
│ └── RandProvider.sol
│ └── token
│ ├── GobblersERC721.sol
│ └── PagesERC721.sol
└── test
├── ArtGobblers.t.sol
├── Benchmarks.t.sol
├── GobblerReserve.t.sol
├── Goo.t.sol
├── Optimizations.t.sol
├── Pages.t.sol
├── RandProvider.t.sol
├── VRGDAs.t.sol
├── correctness
├── GobblersCorrectness.t.sol
└── PagesCorrectness.t.sol
├── deploy
├── DeployMainnet.t.sol
└── DeployRinkeby.t.sol
└── utils
├── Console.sol
├── LibRLP.sol
├── Utilities.sol
└── mocks
├── LinkToken.sol
└── MockGooCalculator.sol
/.gas-snapshot:
--------------------------------------------------------------------------------
1 | ArtGobblersTest:testCanMintMultipleReserved() (gas: 1361868)
2 | ArtGobblersTest:testCanMintPageFromVirtualBalance() (gas: 294329)
3 | ArtGobblersTest:testCanMintReserved() (gas: 669771)
4 | ArtGobblersTest:testCanReuseSacrificedGobblers() (gas: 42630534)
5 | ArtGobblersTest:testCannotMintLegendaryWithLegendary() (gas: 78412007)
6 | ArtGobblersTest:testCannotMintPageWithInsufficientBalance() (gas: 213133)
7 | ArtGobblersTest:testCannotReuseSeedForReveal() (gas: 272886)
8 | ArtGobblersTest:testCannotRevealMoreGobblersThanRemainingToBeRevealed() (gas: 299583)
9 | ArtGobblersTest:testCantAddMoreGooThanOwned() (gas: 206339)
10 | ArtGobblersTest:testCantFeed1155As721() (gas: 1754255)
11 | ArtGobblersTest:testCantFeed721As1155() (gas: 235223)
12 | ArtGobblersTest:testCantFeedGobblers() (gas: 179040)
13 | ArtGobblersTest:testCantFeedUnownedArt() (gas: 140185)
14 | ArtGobblersTest:testCantMintTooFastReserved() (gas: 1238120)
15 | ArtGobblersTest:testCantMintTooFastReservedOneByOne() (gas: 6425073)
16 | ArtGobblersTest:testCantRemoveGoo() (gas: 206478)
17 | ArtGobblersTest:testCantSetRandomSeedWithoutRevealing() (gas: 283681)
18 | ArtGobblersTest:testCantgobbleToUnownedGobbler() (gas: 115134)
19 | ArtGobblersTest:testDoesNotAllowRevealingZero() (gas: 16322)
20 | ArtGobblersTest:testDoesNotRevertEarly() (gas: 7754)
21 | ArtGobblersTest:testDoesRevertWhenExpected() (gas: 10187)
22 | ArtGobblersTest:testEmissionMultipleUpdatesAfterTransfer() (gas: 248786)
23 | ArtGobblersTest:testFeeding1155() (gas: 1763465)
24 | ArtGobblersTest:testFeedingArt() (gas: 258358)
25 | ArtGobblersTest:testFeedingMultiple1155Copies() (gas: 1804979)
26 | ArtGobblersTest:testGobblerBalancesAfterTransfer() (gas: 246810)
27 | ArtGobblersTest:testGooAddition() (gas: 239052)
28 | ArtGobblersTest:testGooRemoval() (gas: 257151)
29 | ArtGobblersTest:testInitialGobblerPrice() (gas: 14704)
30 | ArtGobblersTest:testLegendaryGobblerFinalPrice() (gas: 75771490)
31 | ArtGobblersTest:testLegendaryGobblerMidPrice() (gas: 56867609)
32 | ArtGobblersTest:testLegendaryGobblerMinStartPrice() (gas: 75816084)
33 | ArtGobblersTest:testLegendaryGobblerMintBeforeStart() (gas: 22098)
34 | ArtGobblersTest:testLegendaryGobblerPastFinalPrice() (gas: 71510221)
35 | ArtGobblersTest:testLegendaryGobblerTargetPrice() (gas: 37902581)
36 | ArtGobblersTest:testLegendaryMintBalance() (gas: 40859576)
37 | ArtGobblersTest:testMintFreeLegendaryGobbler() (gas: 75803153)
38 | ArtGobblersTest:testMintFreeLegendaryGobblerPastInterval() (gas: 113688955)
39 | ArtGobblersTest:testMintFromBalanceInsufficient() (gas: 20548)
40 | ArtGobblersTest:testMintFromGoo() (gas: 111206)
41 | ArtGobblersTest:testMintFromGooBalance() (gas: 247755)
42 | ArtGobblersTest:testMintFromMintlist() (gas: 108074)
43 | ArtGobblersTest:testMintFromMintlistBeforeMintingStarts() (gas: 13900)
44 | ArtGobblersTest:testMintInsufficientBalance() (gas: 22919)
45 | ArtGobblersTest:testMintLegendaryGobbler() (gas: 41124253)
46 | ArtGobblersTest:testMintLegendaryGobblerWithInsufficientCost() (gas: 40690290)
47 | ArtGobblersTest:testMintLegendaryGobblerWithSlippage() (gas: 41316040)
48 | ArtGobblersTest:testMintLegendaryGobblerWithUnownedId() (gas: 40972902)
49 | ArtGobblersTest:testMintLegendaryGobblersExpectedIds() (gas: 263713134)
50 | ArtGobblersTest:testMintNotInMintlist() (gas: 11208)
51 | ArtGobblersTest:testMintPriceExceededMax() (gas: 72138)
52 | ArtGobblersTest:testMintReservedGobblersFailsWithNoMints() (gas: 32705)
53 | ArtGobblersTest:testMintedLegendaryURI() (gas: 75826207)
54 | ArtGobblersTest:testMintingFromMintlistTwiceFails() (gas: 107511)
55 | ArtGobblersTest:testMultiReveal() (gas: 8436716)
56 | ArtGobblersTest:testPricingBasic() (gas: 53971135)
57 | ArtGobblersTest:testRevealDelayInitialMint() (gas: 114693)
58 | ArtGobblersTest:testRevealDelayRecurring() (gas: 256456)
59 | ArtGobblersTest:testRevealedUri() (gas: 214307)
60 | ArtGobblersTest:testSimpleRewards() (gas: 212636)
61 | ArtGobblersTest:testSnapshotDoesNotAffectBalance() (gas: 386058)
62 | ArtGobblersTest:testUnmintedLegendaryUri() (gas: 25329)
63 | ArtGobblersTest:testUnmintedUri() (gas: 13456)
64 | ArtGobblersTest:testUnrevealedUri() (gas: 117135)
65 | BenchmarksTest:testAddGoo() (gas: 28678)
66 | BenchmarksTest:testDeployGobblers() (gas: 4111723)
67 | BenchmarksTest:testDeployGoo() (gas: 894945)
68 | BenchmarksTest:testDeployPages() (gas: 1798849)
69 | BenchmarksTest:testGobblerPrice() (gas: 9009)
70 | BenchmarksTest:testGooBalance() (gas: 8686)
71 | BenchmarksTest:testLegendaryGobblersPrice() (gas: 9893)
72 | BenchmarksTest:testMintCommunityPages() (gas: 59039)
73 | BenchmarksTest:testMintGobbler() (gas: 58876)
74 | BenchmarksTest:testMintGobblerUsingVirtualBalance() (gas: 46593)
75 | BenchmarksTest:testMintLegendaryGobbler() (gas: 601736)
76 | BenchmarksTest:testMintPage() (gas: 58812)
77 | BenchmarksTest:testMintPageUsingVirtualBalance() (gas: 54664)
78 | BenchmarksTest:testMintReservedGobblers() (gas: 105805)
79 | BenchmarksTest:testPagePrice() (gas: 9130)
80 | BenchmarksTest:testRemoveGoo() (gas: 28754)
81 | BenchmarksTest:testRevealGobblers() (gas: 2844241)
82 | BenchmarksTest:testTransferGobbler() (gas: 45182)
83 | GobblerReserveTest:testCanWithdraw() (gas: 759640)
84 | GooTest:testBurnAllowed() (gas: 56314)
85 | GooTest:testBurnNotAllowed() (gas: 56354)
86 | GooTest:testMintByAuthority() (gas: 53601)
87 | GooTest:testMintByNonAuthority() (gas: 13113)
88 | GooTest:testSetPages() (gas: 64071)
89 | OptimizationsTest:testFuzzCurrentIdMultipleBranchlessOptimization(uint256) (runs: 256, μ: 408, ~: 423)
90 | PagesTest:testCanMintCommunity() (gas: 651659)
91 | PagesTest:testCanMintMultipleCommunity() (gas: 5979586)
92 | PagesTest:testCantMintTooFastCommunity() (gas: 1174921)
93 | PagesTest:testCantMintTooFastCommunityOneByOne() (gas: 5928020)
94 | PagesTest:testInsufficientBalance() (gas: 20619)
95 | PagesTest:testMintBeforeSetMint() (gas: 20632)
96 | PagesTest:testMintBeforeStart() (gas: 11856)
97 | PagesTest:testMintCommunityPagesFailsWithNoMints() (gas: 30550)
98 | PagesTest:testMintPriceExceededMax() (gas: 69342)
99 | PagesTest:testPagePricingPricingAfterSwitch() (gas: 361050051)
100 | PagesTest:testPagePricingPricingBeforeSwitch() (gas: 224695437)
101 | PagesTest:testRegularMint() (gas: 108780)
102 | PagesTest:testSwitchSmoothness() (gas: 13072)
103 | PagesTest:testTargetPrice() (gas: 14671)
104 | RandProviderTest:testOnlyGobblersCanRequestRandomness() (gas: 8223)
105 | RandProviderTest:testRandomnessIsCorrectlyRequested() (gas: 167249)
106 | RandProviderTest:testRandomnessIsFulfilled() (gas: 175546)
107 | RandProviderTest:testRandomnessIsOnlyUpgradableByOwner() (gas: 351832)
108 | RandProviderTest:testRandomnessIsResetWithPendingSeed() (gas: 573323)
109 | RandProviderTest:testRandomnessIsUpgradable() (gas: 460215)
110 | VRGDAsTest:testFailOverflowForBeyondLimitGobblers(uint256,uint256) (runs: 256, μ: 10310, ~: 10310)
111 | VRGDAsTest:testGobblerPriceStrictlyIncreasesForMostGobblers() (gas: 4111362)
112 | VRGDAsTest:testNoOverflowForAllGobblers(uint256,uint256) (runs: 256, μ: 11218, ~: 11218)
113 | VRGDAsTest:testNoOverflowForFirst8465Pages(uint256,uint256) (runs: 256, μ: 11478, ~: 11326)
114 | VRGDAsTest:testNoOverflowForMostGobblers(uint256,uint256) (runs: 256, μ: 11398, ~: 11240)
115 | VRGDAsTest:testPagePriceStrictlyIncreasesFor8465Pages() (gas: 20653492)
116 | DeployMainnetTest:testColdWallet() (gas: 21871)
117 | DeployMainnetTest:testGobblerClaim() (gas: 113009)
118 | DeployMainnetTest:testGobblerOwnership() (gas: 13714)
119 | DeployMainnetTest:testGooAddressCorrectness() (gas: 18258)
120 | DeployMainnetTest:testPagesAddressCorrectness() (gas: 18302)
121 | DeployMainnetTest:testRoot() (gas: 11438)
122 | DeployMainnetTest:testURIs() (gas: 48600)
123 | DeployRinkebyTest:testColdWallet() (gas: 21815)
124 | DeployRinkebyTest:testGooAddressCorrectness() (gas: 18214)
125 | DeployRinkebyTest:testMerkleRoot() (gas: 109695)
126 | DeployRinkebyTest:testPagesAddressCorrectness() (gas: 18213)
127 | DeployRinkebyTest:testURIs() (gas: 48574)
128 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | tests:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: actions/setup-node@v2
11 |
12 | - name: Install dev dependencies
13 | run: npm install
14 |
15 | - name: Install Foundry
16 | uses: onbjerg/foundry-toolchain@v1
17 | with:
18 | version: nightly
19 |
20 | - name: Run lint check
21 | run: npm run lint:check
22 |
23 | - name: Install dependencies
24 | run: forge install
25 |
26 | - name: Check contract sizes
27 | run: forge build --sizes
28 |
29 | - name: Check gas snapshots
30 | run: forge snapshot --check
31 |
32 | - name: Run tests
33 | run: forge test
34 | env:
35 | # Only fuzz intensely if we're running this action on a push to master or for a PR going into master:
36 | FOUNDRY_PROFILE: ${{ (github.ref == 'refs/heads/master' || github.base_ref == 'master') && 'intense' }}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | out/
2 | cache/
3 | node_modules/
4 | .env
5 | .DS_Store
6 | env
7 | __pycache__/
8 | lcov.info
9 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/forge-std"]
2 | path = lib/forge-std
3 | url = https://github.com/foundry-rs/forge-std
4 | [submodule "lib/VRGDAs"]
5 | path = lib/VRGDAs
6 | url = https://github.com/transmissions11/VRGDAs
7 | [submodule "lib/goo-issuance"]
8 | path = lib/goo-issuance
9 | url = https://github.com/transmissions11/goo-issuance
10 | [submodule "lib/chainlink"]
11 | path = lib/chainlink
12 | url = https://github.com/smartcontractkit/chainlink
13 | [submodule "lib/ds-test"]
14 | path = lib/ds-test
15 | url = https://github.com/dapphub/ds-test
16 | [submodule "lib/solmate"]
17 | path = lib/solmate
18 | url = https://github.com/transmissions11/solmate
19 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /lib
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "printWidth": 100,
4 |
5 | "overrides": [
6 | {
7 | "files": "*.sol",
8 | "options": {
9 | "tabWidth": 4,
10 | "printWidth": 120
11 | }
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.solhint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "solhint:recommended",
3 | "rules": {
4 | "compiler-version": ["error",">=0.8.0"],
5 | "avoid-low-level-calls": "off"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "solidity.packageDefaultDependenciesContractsDirectory": "src",
3 | "solidity.packageDefaultDependenciesDirectory": "lib",
4 | "solidity.compileUsingRemoteVersion": "v0.8.13",
5 | "search.exclude": { "lib": true },
6 | "files.associations": {
7 | ".gas-snapshot": "julia"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # Art Gobblers Contributors
2 |
3 | | Contributor | Twitter |
4 | | --------------- | --------------------------------------------------------- |
5 | | FrankieIsLost | [`@FrankieIsLost`](https://twitter.com/FrankieIsLost) |
6 | | transmissions11 | [`@transmissions11`](https://twitter.com/transmissions11) |
7 | | samczsun | [`@samczsun`](https://twitter.com/samczsun) |
8 | | Riley Holterhus | [`@rileyholterhus`](https://twitter.com/rileyholterhus) |
9 | | fiveoutofnine | [`@fiveoutofnine`](https://twitter.com/fiveoutofnine) |
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Art Gobblers
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Art Gobblers • [](https://github.com/artgobblers/art-gobblers/actions/workflows/tests.yml)
4 |
5 | Art Gobblers is an experimental decentralized art factory by Justin Roiland and Paradigm.
6 |
7 | ## Background
8 |
9 | Art Gobblers is a decentralized art factory owned by aliens. As artists make cool art, Gobblers gains cultural relevance, making collectors want the art more, incentivizing artists to make cooler art. It's also an on-chain game.
10 |
11 | See our [overview of the system](https://www.paradigm.xyz/2022/09/artgobblers), as well as deep dives into some of the project's mechanisms, like [GOO](https://www.paradigm.xyz/2022/09/goo) and [VRGDAs](https://www.paradigm.xyz/2022/08/vrgda).
12 |
13 | ## Deployments
14 |
15 | | Contract | Mainnet | Goerli |
16 | |---------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
17 | | `ArtGobblers` | [`0x60bb1e2aa1c9acafb4d34f71585d7e959f387769`](https://etherscan.io/address/0x60bb1e2aa1c9acafb4d34f71585d7e959f387769) | [`0x60bb1e2aa1c9acafb4d34f71585d7e959f387769`](https://goerli.etherscan.io/address/0x60bb1e2aa1c9acafb4d34f71585d7e959f387769) |
18 | | `Pages` | [`0x600df00d3e42f885249902606383ecdcb65f2e02`](https://etherscan.io/address/0x600df00d3e42f885249902606383ecdcb65f2e02) | [`0x600df00d3e42f885249902606383ecdcb65f2e02`](https://goerli.etherscan.io/address/0x600df00d3e42f885249902606383ecdcb65f2e02) |
19 | | `Goo` | [`0x600000000a36f3cd48407e35eb7c5c910dc1f7a8`](https://etherscan.io/address/0x600000000a36f3cd48407e35eb7c5c910dc1f7a8) | [`0x600000000a36f3cd48407e35eb7c5c910dc1f7a8`](https://goerli.etherscan.io/address/0x600000000a36f3cd48407e35eb7c5c910dc1f7a8) |
20 |
21 | ## State Diagrams
22 |
23 |
24 | 
25 | 
26 | 
27 |
28 | ## Usage
29 |
30 | You will need a copy of [Foundry](https://github.com/foundry-rs/foundry) installed before proceeding. See the [installation guide](https://github.com/foundry-rs/foundry#installation) for details.
31 |
32 | To build the contracts:
33 |
34 | ```sh
35 | git clone https://github.com/artgobblers/art-gobblers.git
36 | cd art-gobblers
37 | forge install
38 | ```
39 |
40 | ### Run Tests
41 |
42 | In order to run unit tests, run:
43 |
44 | ```sh
45 | forge test
46 | ```
47 |
48 | For longer fuzz campaigns, run:
49 |
50 | ```sh
51 | FOUNDRY_PROFILE="intense" forge test
52 | ```
53 |
54 | For differential fuzzing against a python implementation, see [here](./analysis/README.md).
55 |
56 | ### Run Slither
57 |
58 | After [installing Slither](https://github.com/crytic/slither#how-to-install), run:
59 |
60 | ```sh
61 | slither src/ --solc-remaps 'ds-test/=lib/ds-test/src/ solmate/=lib/solmate/src/ forge-std/=lib/forge-std/src/ chainlink/=lib/chainlink/contracts/src/ VRGDAs/=lib/VRGDAs/src/ goo-issuance/=lib/goo-issuance/src/'
62 | ```
63 |
64 |
65 | ### Update Gas Snapshots
66 |
67 | To update the gas snapshots, run:
68 |
69 | ```sh
70 | forge snapshot
71 | ```
72 |
73 | ### Deploy Contracts
74 |
75 | In order to deploy the art gobblers contracts, set the relevant constants in the `DeployMainnet` script, and run the following command(s):
76 |
77 | ```sh
78 | export DEPLOYER_PRIVATE_KEY=$DEPLOYER_PRIVATE_KEY
79 | export GOBBLER_PRIVATE_KEY=$GOBBLER_PRIVATE_KEY
80 | export PAGES_PRIVATE_KEY=$PAGES_PRIVATE_KEY
81 | export GOO_PRIVATE_KEY=$GOO_PRIVATE_KEY
82 |
83 | forge script script/deploy/DeployMainnet.s.sol:DeployMainnet --rpc-url $RPC_URL --verify --etherscan-api-key $API_KEY
84 | ```
85 |
86 | We use [profanity2](https://github.com/1inch/profanity2) to securely generate vanity addresses for the `ArtGobblers`, `Pages`, and `Goo` contracts. As a result, each of these contracts must be deployed using a unique private key. To simplify deployment, the deployment script ensures that only `DEPLOYER_PRIVATE_KEY` needs to be seeded with ETH, by automatically transferring 0.25 ETH from it to the other deployers before they are used.
87 |
88 | To ensure security in case the private keys generated by [profanity2](https://github.com/1inch/profanity2) are [compromised](https://blog.1inch.io/a-vulnerability-disclosed-in-profanity-an-ethereum-vanity-address-tool-68ed7455fc8c), the script immediately revokes `GOBBLER_PRIVATE_KEY`'s ownership over `ArtGobblers` and transfers it to a configurable address. `Pages` and `Goo` do not grant any special authority to their respective deployers.
89 |
90 | ## Audits
91 |
92 | The following auditors were engaged to review the project before launch:
93 |
94 | - [samczsun](https://samczsun.com) (No report)
95 | - [Spearbit](https://spearbit.com) (Report [here](https://github.com/spearbit/portfolio/blob/master/pdfs/ArtGobblers-Spearbit-Security-Review.pdf))
96 | - [code4rena](https://code423n4.com) (Report [here](https://code4rena.com/reports/2022-09-artgobblers))
97 | - [Riley Holterhus](https://www.rileyholterhus.com) (No Report)
98 |
99 | ## License
100 |
101 | [MIT](LICENSE) © 2022 Art Gobblers
102 |
--------------------------------------------------------------------------------
/analysis/README.md:
--------------------------------------------------------------------------------
1 | # Gobblers Analysis
2 |
3 | Additional analysis for Art Gobblers, including python implementations for differential fuzzing and automated theorem proofs of certain key assumptions.
4 |
5 | ## Differential fuzzing
6 |
7 | In order to run differential fuzz tests, first install requirements with:
8 |
9 | ```
10 | pip install -r requirements.txt
11 | ```
12 |
13 | Then run FFI tests from the root directory:
14 |
15 | ```
16 | FOUNDRY_PROFILE="FFI" forge test
17 | ```
18 |
19 | ## Automated theorem proving
20 |
21 | In order to run, first install a theorem prover such as [Z3](https://github.com/Z3Prover/z3).
22 |
23 | Then, run the following command:
24 |
25 | ```
26 | z3 smt/goo_pooling.smt2
27 | ```
28 |
--------------------------------------------------------------------------------
/analysis/python/compute_price.py:
--------------------------------------------------------------------------------
1 | from pricer import Pricer
2 | from eth_abi import encode_single
3 | import argparse
4 |
5 | def main(args):
6 | if (args.type == 'gobblers'):
7 | calculate_gobblers_price(args)
8 | elif (args.type == 'pages'):
9 | calculate_pages_price(args)
10 |
11 | def calculate_gobblers_price(args):
12 | pricer = Pricer()
13 | price = pricer.compute_gobbler_price(
14 | args.time_since_start / (60 * 60 * 24), ## convert to seconds
15 | args.num_sold,
16 | args.initial_price / (10 ** 18), ## scale decimals
17 | args.per_period_price_decrease / (10 ** 18), ## scale decimals
18 | args.logistic_scale / (10 ** 18), ## scale decimals
19 | args.time_scale / (10 ** 18), ## scale decimals
20 | 0
21 | )
22 | price *= (10 ** 18)
23 | encode_and_print(price)
24 |
25 | def calculate_pages_price(args):
26 | pricer = Pricer()
27 | price = pricer.compute_page_price(
28 | args.time_since_start / (60 * 60 * 24), ## convert to seconds
29 | args.num_sold,
30 | args.initial_price / (10 ** 18), ## scale decimals
31 | args.per_period_price_decrease / (10 ** 18), ## scale decimals
32 | args.logistic_scale / (10 ** 18), ## scale decimals
33 | args.time_scale / (10 ** 18), ## scale decimals
34 | 0,
35 | args.per_period_post_switchover / (10 ** 18), ## scale decimals
36 | args.switchover_time / (10 ** 18)
37 | )
38 | price *= (10 ** 18)
39 | encode_and_print(price)
40 |
41 | def encode_and_print(price):
42 | enc = encode_single('uint256', int(price))
43 | ## append 0x for FFI parsing
44 | print("0x" + enc.hex())
45 |
46 | def parse_args():
47 | parser = argparse.ArgumentParser()
48 | parser.add_argument("type", choices=["gobblers", "pages"])
49 | parser.add_argument("--time_since_start", type=int)
50 | parser.add_argument("--num_sold", type=int)
51 | parser.add_argument("--initial_price", type=int)
52 | parser.add_argument("--per_period_price_decrease", type=int)
53 | parser.add_argument("--logistic_scale", type=int)
54 | parser.add_argument("--time_scale", type=int)
55 | parser.add_argument("--per_period_post_switchover", type=int)
56 | parser.add_argument("--switchover_time", type=int)
57 | return parser.parse_args()
58 |
59 | if __name__ == '__main__':
60 | args = parse_args()
61 | main(args)
62 |
--------------------------------------------------------------------------------
/analysis/python/pricer.py:
--------------------------------------------------------------------------------
1 | import math
2 | import numpy as np
3 |
4 |
5 | class Pricer:
6 |
7 | def compute_gobbler_price(self, time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift):
8 | return self.compute_vrgda_price(time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift)
9 |
10 | def compute_page_price(self, time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift, per_period_post_switchover, switchover_time):
11 | initial_value = logistic_scale/ (1 +math.exp(time_scale * time_shift))
12 | sold_by_switchover = logistic_scale / (1 + math.exp(-1 * time_scale * (switchover_time - time_shift))) - initial_value
13 | if num_sold < sold_by_switchover:
14 | return self.compute_vrgda_price(time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift)
15 | else:
16 | f_inv = (num_sold - sold_by_switchover) / per_period_post_switchover + switchover_time
17 | return initial_price * math.exp(-math.log(1 - per_period_price_decrease) * (f_inv - time_since_start))
18 |
19 | def compute_vrgda_price(self, time_since_start, num_sold, initial_price, per_period_price_decrease, logistic_scale, time_scale, time_shift):
20 | initial_value = logistic_scale / (1 + math.exp(time_scale * time_shift))
21 | logistic_value = num_sold + initial_value
22 | price = (1 - per_period_price_decrease) ** (time_since_start - time_shift + (math.log(-1 + logistic_scale / logistic_value) / time_scale)) * initial_price
23 | return price
24 |
--------------------------------------------------------------------------------
/analysis/requirements.txt:
--------------------------------------------------------------------------------
1 | cytoolz==0.11.2
2 | eth-abi==3.0.0
3 | eth-hash==0.3.2
4 | eth-typing==3.0.0
5 | eth-utils==2.0.0
6 | numpy==1.22.3
7 | parsimonious==0.8.1
8 | six==1.16.0
9 | toolz==0.11.2
10 |
--------------------------------------------------------------------------------
/analysis/smt/goo_pooling.smt2:
--------------------------------------------------------------------------------
1 | ; authors: hrkrshnn, leonardoalt
2 | ; prove that combined staking is always at least as good as staking goo into separate gobblers
3 | ; should be unsat
4 |
5 | (define-fun sqrt2 ((n Real) (r Real)) Bool
6 | (= n (* r r))
7 | )
8 |
9 | (define-fun delta_s ((m Real) (t Real) (s Real) (sqr Real) (res Real)) Bool
10 | (and
11 | (sqrt2 (* s m) sqr)
12 | (=
13 | res
14 | (+
15 | (*
16 | (/ 1 4)
17 | m
18 | (* t t)
19 | )
20 | (* sqr t)
21 | )
22 | )
23 | )
24 | )
25 |
26 | (define-fun delta_s_comb ((m1 Real) (m2 Real) (t Real) (s1 Real) (s2 Real) (sqr Real) (res Real)) Bool
27 | (and
28 | (sqrt2 (* (+ s1 s2) (+ m1 m2)) sqr)
29 | (=
30 | res
31 | (+
32 | (*
33 | (/ 1 4)
34 | (+ m1 m2)
35 | (* t t)
36 | )
37 | (* sqr t)
38 | )
39 | )
40 | )
41 | )
42 |
43 | (declare-const t Real)
44 | (declare-const s1 Real)
45 | (declare-const s2 Real)
46 | (declare-const m1 Real)
47 | (declare-const m2 Real)
48 |
49 | (declare-const ds_1 Real)
50 | (declare-const ds_2 Real)
51 | (declare-const ds_comb Real)
52 |
53 | (declare-const sqr_1 Real)
54 | (declare-const sqr_2 Real)
55 | (declare-const sqr_3 Real)
56 |
57 | (assert (and
58 | (>= t 0)
59 | (>= s1 0)
60 | (>= s2 0)
61 | (>= m1 0)
62 | (>= m2 0)
63 | (>= sqr_1 0)
64 | (>= sqr_2 0)
65 | (>= sqr_3 0)
66 | ))
67 |
68 | (assert (and
69 | (delta_s m1 t s1 sqr_1 ds_1)
70 | (delta_s m2 t s2 sqr_2 ds_2)
71 | (delta_s_comb m1 m2 t s1 s2 sqr_3 ds_comb)
72 | ))
73 |
74 | (assert
75 | (< ds_comb (+ ds_1 ds_2))
76 | )
77 |
78 | (check-sat)
--------------------------------------------------------------------------------
/assets/gobbler.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artgobblers/art-gobblers/a18ea7fec3b766444a3277392c405e2a107a2d75/assets/gobbler.png
--------------------------------------------------------------------------------
/assets/state-machines/gobbler-lifecycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artgobblers/art-gobblers/a18ea7fec3b766444a3277392c405e2a107a2d75/assets/state-machines/gobbler-lifecycle.png
--------------------------------------------------------------------------------
/assets/state-machines/legendary-gobbler-auctions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artgobblers/art-gobblers/a18ea7fec3b766444a3277392c405e2a107a2d75/assets/state-machines/legendary-gobbler-auctions.png
--------------------------------------------------------------------------------
/assets/state-machines/page-auctions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artgobblers/art-gobblers/a18ea7fec3b766444a3277392c405e2a107a2d75/assets/state-machines/page-auctions.png
--------------------------------------------------------------------------------
/foundry.toml:
--------------------------------------------------------------------------------
1 | [profile.default]
2 | solc = "0.8.13"
3 | optimizer_runs = 1000000
4 | bytecode_hash = "none"
5 | no_match_test = "FFI|LongRunning"
6 |
7 | [profile.intense]
8 | no_match_test = "FFI"
9 |
10 | [profile.intense.fuzz]
11 | runs = 10000
12 |
13 | [profile.ffi]
14 | ffi = true
15 | no_match_test = "LongRunning"
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "art-gobblers",
3 | "author": "artgobblers",
4 | "version": "1.0.0",
5 | "description": "Art Gobblers is an experimental decentralized art factory by Justin Roiland and Paradigm.",
6 | "homepage": "https://github.com/FrankieIsLost/art-gobblers#readme",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/FrankieIsLost/art-gobblers.git"
10 | },
11 | "scripts": {
12 | "prettier": "prettier --write **.sol",
13 | "prettier:list": "prettier --list-different **.sol",
14 | "prettier:check": "prettier --check **.sol",
15 | "solhint": "solhint --config ./.solhint.json 'src/**/*.sol' --fix",
16 | "solhint:check": "solhint --config ./.solhint.json 'src/**/*.sol'",
17 | "lint": "npm run prettier && npm run solhint",
18 | "lint:check": "npm run prettier:check && npm run solhint:check"
19 | },
20 | "devDependencies": {
21 | "prettier": "^2.5.1",
22 | "prettier-plugin-solidity": "^1.0.0-beta.19",
23 | "solhint": "^3.3.6"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/remappings.txt:
--------------------------------------------------------------------------------
1 | VRGDAs/=lib/VRGDAs/src/
2 | ds-test/=lib/ds-test/src/
3 | solmate/=lib/solmate/src/
4 | forge-std/=lib/forge-std/src/
5 | goo-issuance/=lib/goo-issuance/src/
6 | chainlink/=lib/chainlink/contracts/src/
--------------------------------------------------------------------------------
/script/deploy/DeployBase.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import "forge-std/Script.sol";
5 |
6 | import {LibRLP} from "../../test/utils/LibRLP.sol";
7 |
8 | import {GobblerReserve} from "../../src/utils/GobblerReserve.sol";
9 | import {RandProvider} from "../../src/utils/rand/RandProvider.sol";
10 | import {ChainlinkV1RandProvider} from "../../src/utils/rand/ChainlinkV1RandProvider.sol";
11 |
12 | import {Goo} from "../../src/Goo.sol";
13 | import {Pages} from "../../src/Pages.sol";
14 | import {ArtGobblers} from "../../src/ArtGobblers.sol";
15 |
16 | abstract contract DeployBase is Script {
17 | // Environment specific variables.
18 | address private immutable governorWallet;
19 | address private immutable teamColdWallet;
20 | address private immutable communityWallet;
21 | bytes32 private immutable merkleRoot;
22 | uint256 private immutable mintStart;
23 | address private immutable vrfCoordinator;
24 | address private immutable linkToken;
25 | bytes32 private immutable chainlinkKeyHash;
26 | uint256 private immutable chainlinkFee;
27 | string private gobblerBaseUri;
28 | string private gobblerUnrevealedUri;
29 | string private pagesBaseUri;
30 | bytes32 private immutable provenanceHash;
31 |
32 | // Deploy addresses.
33 | GobblerReserve public teamReserve;
34 | GobblerReserve public communityReserve;
35 | Goo public goo;
36 | RandProvider public randProvider;
37 | ArtGobblers public artGobblers;
38 | Pages public pages;
39 |
40 | constructor(
41 | address _governorWallet,
42 | address _teamColdWallet,
43 | address _communityWallet,
44 | bytes32 _merkleRoot,
45 | uint256 _mintStart,
46 | address _vrfCoordinator,
47 | address _linkToken,
48 | bytes32 _chainlinkKeyHash,
49 | uint256 _chainlinkFee,
50 | string memory _gobblerBaseUri,
51 | string memory _gobblerUnrevealedUri,
52 | string memory _pagesBaseUri,
53 | bytes32 _provenanceHash
54 | ) {
55 | governorWallet = _governorWallet;
56 | teamColdWallet = _teamColdWallet;
57 | communityWallet = _communityWallet;
58 | merkleRoot = _merkleRoot;
59 | mintStart = _mintStart;
60 | vrfCoordinator = _vrfCoordinator;
61 | linkToken = _linkToken;
62 | chainlinkKeyHash = _chainlinkKeyHash;
63 | chainlinkFee = _chainlinkFee;
64 | gobblerBaseUri = _gobblerBaseUri;
65 | gobblerUnrevealedUri = _gobblerUnrevealedUri;
66 | pagesBaseUri = _pagesBaseUri;
67 | provenanceHash = _provenanceHash;
68 | }
69 |
70 | function run() external {
71 | uint256 deployerKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
72 | uint256 gobblerKey = vm.envUint("GOBBLER_PRIVATE_KEY");
73 | uint256 pagesKey = vm.envUint("PAGES_PRIVATE_KEY");
74 | uint256 gooKey = vm.envUint("GOO_PRIVATE_KEY");
75 |
76 | address gobblerDeployerAddress = vm.addr(gobblerKey);
77 | address pagesDeployerAddress = vm.addr(pagesKey);
78 | address gooDeployerAddress = vm.addr(gooKey);
79 |
80 | // Precomputed contract addresses, based on contract deploy nonces.
81 | address gobblerAddress = LibRLP.computeAddress(gobblerDeployerAddress, 0);
82 | address pageAddress = LibRLP.computeAddress(pagesDeployerAddress, 0);
83 |
84 | vm.startBroadcast(deployerKey);
85 |
86 | // Deploy team and community reserves, owned by cold wallet.
87 | teamReserve = new GobblerReserve(ArtGobblers(gobblerAddress), teamColdWallet);
88 | communityReserve = new GobblerReserve(ArtGobblers(gobblerAddress), teamColdWallet);
89 | randProvider = new ChainlinkV1RandProvider(
90 | ArtGobblers(gobblerAddress),
91 | vrfCoordinator,
92 | linkToken,
93 | chainlinkKeyHash,
94 | chainlinkFee
95 | );
96 |
97 | // Fund each of the other deployer addresses.
98 | payable(gobblerDeployerAddress).transfer(0.25 ether);
99 | payable(pagesDeployerAddress).transfer(0.25 ether);
100 | payable(gooDeployerAddress).transfer(0.25 ether);
101 |
102 | vm.stopBroadcast();
103 |
104 | vm.startBroadcast(gooKey);
105 |
106 | // Deploy goo contract.
107 | goo = new Goo(
108 | // Gobblers contract address:
109 | gobblerAddress,
110 | // Pages contract address:
111 | pageAddress
112 | );
113 |
114 | vm.stopBroadcast();
115 |
116 | vm.startBroadcast(gobblerKey);
117 |
118 | // Deploy gobblers contract,
119 | artGobblers = new ArtGobblers(
120 | merkleRoot,
121 | mintStart,
122 | goo,
123 | Pages(pageAddress),
124 | address(teamReserve),
125 | address(communityReserve),
126 | randProvider,
127 | gobblerBaseUri,
128 | gobblerUnrevealedUri,
129 | provenanceHash
130 | );
131 |
132 | artGobblers.transferOwnership(governorWallet);
133 |
134 | vm.stopBroadcast();
135 |
136 | vm.startBroadcast(pagesKey);
137 |
138 | // Deploy pages contract.
139 | pages = new Pages(mintStart, goo, communityWallet, artGobblers, pagesBaseUri);
140 |
141 | vm.stopBroadcast();
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/script/deploy/DeployGoerli.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DeployBase} from "./DeployBase.s.sol";
5 |
6 | contract DeployGoerli is DeployBase {
7 | address public immutable coldWallet = 0xE974159205528502237758439da8c4dcc03D3023;
8 | address public immutable communityWallet = 0xDf2aAeead21Cf2BFF3965E858332aC8c8364E991;
9 | address public immutable governorWallet = 0x2719E6FdDd9E33c077866dAc6bcdC40eB54cD4f7;
10 |
11 | bytes32 public immutable root = 0xae49de097f1b61ff3ff428b660ddf98b6a8f64ed0f9b665709b13d3721b79405;
12 |
13 | // Monday, October 31, 2022 8:20:00 PM
14 | uint256 public immutable mintStart = 1667247600;
15 |
16 | string public constant gobblerBaseUri = "https://nfts.artgobblers.com/api/gobblers/";
17 | string public constant gobblerUnrevealedUri = "https://nfts.artgobblers.com/api/gobblers/unrevealed";
18 | string public constant pagesBaseUri = "https://nfts.artgobblers.com/api/pages/";
19 |
20 | bytes32 public immutable provenance = 0x628f3ac523165f5cf33334938a6211f0065ce6dc20a095d5274c34df8504d6e4;
21 |
22 | constructor()
23 | DeployBase(
24 | // Governor wallet:
25 | governorWallet,
26 | // Team cold wallet:
27 | coldWallet,
28 | // Community wallet:
29 | communityWallet,
30 | // Merkle root:
31 | root,
32 | // Mint start:
33 | mintStart,
34 | // VRF coordinator:
35 | address(0x2bce784e69d2Ff36c71edcB9F88358dB0DfB55b4),
36 | // LINK token:
37 | address(0x326C977E6efc84E512bB9C30f76E30c160eD06FB),
38 | // Chainlink hash:
39 | 0x0476f9a745b61ea5c0ab224d3a6e4c99f0b02fce4da01143a4f70aa80ae76e8a,
40 | // Chainlink fee:
41 | 0.1e18,
42 | // Gobbler base URI:
43 | gobblerBaseUri,
44 | // Gobbler unrevealed URI:
45 | gobblerUnrevealedUri,
46 | // Pages base URI:
47 | pagesBaseUri,
48 | // Provenance hash:
49 | provenance
50 | )
51 | {}
52 | }
53 |
--------------------------------------------------------------------------------
/script/deploy/DeployMainnet.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DeployBase} from "./DeployBase.s.sol";
5 |
6 | contract DeployMainnet is DeployBase {
7 | address public immutable coldWallet = 0xE974159205528502237758439da8c4dcc03D3023;
8 | address public immutable communityWallet = 0xDf2aAeead21Cf2BFF3965E858332aC8c8364E991;
9 | address public immutable governorWallet = 0x2719E6FdDd9E33c077866dAc6bcdC40eB54cD4f7;
10 |
11 | bytes32 public immutable root = 0xae49de097f1b61ff3ff428b660ddf98b6a8f64ed0f9b665709b13d3721b79405;
12 |
13 | // Monday, October 31, 2022 8:20:00 PM
14 | uint256 public immutable mintStart = 1667247600;
15 |
16 | string public constant gobblerBaseUri = "https://nfts.artgobblers.com/api/gobblers/";
17 | string public constant gobblerUnrevealedUri = "https://nfts.artgobblers.com/api/gobblers/unrevealed";
18 | string public constant pagesBaseUri = "https://nfts.artgobblers.com/api/pages/";
19 |
20 | bytes32 public immutable provenance = 0x628f3ac523165f5cf33334938a6211f0065ce6dc20a095d5274c34df8504d6e4;
21 |
22 | constructor()
23 | DeployBase(
24 | // Governor wallet:
25 | governorWallet,
26 | // Team cold wallet:
27 | coldWallet,
28 | // Community wallet:
29 | communityWallet,
30 | // Merkle root:
31 | root,
32 | // Mint start:
33 | mintStart,
34 | // VRF coordinator:
35 | address(0xf0d54349aDdcf704F77AE15b96510dEA15cb7952),
36 | // LINK token:
37 | address(0x514910771AF9Ca656af840dff83E8264EcF986CA),
38 | // Chainlink hash:
39 | 0xAA77729D3466CA35AE8D28B3BBAC7CC36A5031EFDC430821C02BC31A238AF445,
40 | // Chainlink fee:
41 | 2e18,
42 | // Gobbler base URI:
43 | gobblerBaseUri,
44 | // Gobbler unrevealed URI:
45 | gobblerUnrevealedUri,
46 | // Pages base URI:
47 | pagesBaseUri,
48 | // Provenance hash:
49 | provenance
50 | )
51 | {}
52 | }
53 |
--------------------------------------------------------------------------------
/script/deploy/DeployRinkeby.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DeployBase} from "./DeployBase.s.sol";
5 |
6 | contract DeployRinkeby is DeployBase {
7 | address public immutable coldWallet = 0x126620598A797e6D9d2C280b5dB91b46F27A8330;
8 | address public immutable communityWallet = 0x126620598A797e6D9d2C280b5dB91b46F27A8330;
9 | address public immutable governorWallet = 0x2719E6FdDd9E33c077866dAc6bcdC40eB54cD4f7;
10 |
11 | address public immutable root = 0x1D18077167c1177253555e45B4b5448B11E30b4b;
12 |
13 | // Monday, June 27, 2022 10:42:48 PM
14 | uint256 public immutable mintStart = 1656369768;
15 |
16 | string public constant gobblerBaseUri = "https://testnet.ag.xyz/api/nfts/gobblers/";
17 | string public constant gobblerUnrevealedUri = "https://testnet.ag.xyz/api/nfts/unrevealed";
18 | string public constant pagesBaseUri = "https://testnet.ag.xyz/api/nfts/pages/";
19 |
20 | address public immutable provenance = address(0xBEEB00);
21 |
22 | constructor()
23 | DeployBase(
24 | // Governor wallet:
25 | governorWallet,
26 | // Team cold wallet:
27 | coldWallet,
28 | // Community wallet:
29 | communityWallet,
30 | // Merkle root:
31 | keccak256(abi.encodePacked(root)),
32 | // Mint start:
33 | mintStart,
34 | // VRF coordinator:
35 | address(0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B),
36 | // LINK token:
37 | address(0x01BE23585060835E02B77ef475b0Cc51aA1e0709),
38 | // Chainlink hash:
39 | 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311,
40 | // Chainlink fee:
41 | 0.1e18,
42 | // Gobbler base URI:
43 | gobblerBaseUri,
44 | // Gobbler unrevealed URI:
45 | gobblerUnrevealedUri,
46 | // Pages base URI:
47 | pagesBaseUri,
48 | // Provenance hash:
49 | keccak256(abi.encodePacked(provenance))
50 | )
51 | {}
52 | }
53 |
--------------------------------------------------------------------------------
/src/ArtGobblers.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | /* **,/*,
5 | *%@&%#/*,,..........,/(%&@@#*
6 | %@%,..............................#@@%
7 | &&,.....,,...............................,/&@*
8 | (@*.....**............,/,.......................(@%
9 | &&......*,............./,.............**............&@
10 | @#......**.............**..............,*........,*,..,@/
11 | /@......,/............../,..............,*........../,..*@.
12 | #@,......................*.............../,..........**...#/
13 | ,@&,.......................................*..........,/....(@
14 | *@&(*...................................................../*....(@
15 | @(..*%@@&%#(#@@@%%%%%&&@@@@@@@@@&(///..........................#@
16 | @%/@@@&%&&&@@&%%%%%%%#(/(((/(/(/(/(/(/(/(/(%%&@@@%(/,............#&
17 | @@@#/**./@%%%&%#/*************./(%@@@@&(*********(@&&@@@%(.....,&@
18 | ,@/.//(&@@/. .#@%/******./&&*, ./@&********%@/**(@#@@#,..(@
19 | #%****%@. %@/****./&@ ,. %&********%@(**&@...(@#.#@
20 | **./@/ %@&& .@#****./@* &@@@@& .@/******./@@((((@&....(@
21 | ##**./&@ ,&@@@, #@/****./@@ @@. .@&*******./@%****%@@@(,
22 | ,@/**./%@(. .*@@/********(&@#*,,,,/&@%/*******./@@&&&@@@#
23 | @&/**@&/%&&&&&%/**.//////*********./************./@&******@*
24 | /@@@@&(////#%&@@&(**./#&@@&(//*************./&@(********#@
25 | .@#**.///*****************(#@@@&&&&&@@@@&%(**********./@,
26 | @(*****%@#*********************&@#*********************(@
27 | @****./@#*./@@#//***.///(%@%*****%@*********************#@
28 | #&****./@%************************&@**********************@%
29 | .@/******.//*******************./@@(************************@/
30 | /@**********************************************************(@,
31 | @#*****************************************************%@@@@@@@.
32 | *@/*************************************************************#@(
33 | @%***************************************************************./@(
34 | /@@&&&@@ .@/*******************************************************************&@
35 | @%######%@. @#***************************./%&&&%(**************#%******************
36 | @%######&@%&@@. ,@(***./********************#@####%@&*************&%****************./@,
37 | &&*,/@%######&@@@*.*@&, @@****./@&*******************./%@#######%@#***********./@&*****************(@
38 | ((...*%@#%@@,..........,,,,%@&@%/*****&%****************./&@#*%@#######&@*#@%*********./@&*****************(@,
39 | (@#....(@%#&&,...,/...........@(*******(@(****************(@/...*%@@@@@@%*....&@@@@&@@@@@@%/%@@##(************(@.
40 | ((./(((%@%#&@/,/&@/...........%&*******%@****************./@%,.................#,............/@%***************#@
41 | *@@####@@%###%&@(@(...........%&*******%@****************%@,,#%/..............................#@/***************&/
42 | (#.....,&###&@..%%..........%%*****(@@#****************#@,...................................@(***************(@
43 | .@@&%%&@@###&&.............,@(***%@(**********./#%%%%%##&@(,...............................#@****************&.
44 | .....(@%###&@*............%@**%@(*******(&@&%#/////////@%...................................#@***************&@
45 | #@@@@&%####&@&&&,........%@./@%*****(@@%////////////////@@@%,...............................#@**************#@
46 | @@&&&&@@( /&@@&%%@&@@@%**./&@(///////////////////@%.................................,@(*********./%@&.
47 | (@//@% @%***&&(//////////////////////(&@(**,,,,./(%&@@@%/*,,****,,***./@@&&&&&&&//%@
48 | (@//%@ (@(*#@#////////////////////////////%@@%%%&@@#////%@/***************************&&
49 | (@//%@ .,,,,/#&&&&&&@&*#@#///////////////////////////////@%//&&///////#@(***************************@&(#@@@@@&(*.
50 | ,@@@@@&&@//%@,,.,,,,,.,..,,#@./@%////////////////////////////////%@**&&////////(@(**************************,,,,,,,,,,,,/(#&@&
51 | &@%*,,,,,,,,#@//%@,,,,,,,,,,,,,,&%*#@(////////////////////////////////%@**&&/////////&@**************************#@.,,,.,,.,,.,,...,%@
52 | (@/,,,,,,,,,,,,(@(/%@,,,,,,,,,,,,,,&%*#@(////////////////////////////////%@./%@/////////#@(*************************&%,,,(%@@@@#*,. .,/@.
53 | &%.. *&@%/,.,#@(*#@*,,.,,,,,,,,,,%@/#@(////////////////////////////////%@**#@/////////#@(*****************.//#%@@@@%%(/,... ...,,,%&
54 | ,@*.,. ../((%&&@@@&%#((///,,,,,/@&(@(////////////////////////////////@&**#@/////////%@%###%&&&&@@@@@@%%#(**,,,,,,. ..,,,,,,,,,,%#
55 | @(,,,,,.., ,.. ..,,,**(%%%&@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%(((,,.,.,,,,,,.,..,,,,.,.,,,,.,..,.,,.,,,,,.,,,,,,,,,.*@%
56 | @%,,,,,,,,,,,,,,,.,.,,, .,.,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,.,,,.,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,.,.,,,,,,#@@,
57 | ,@@(,.,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,.,,.,,.,.,./#%&@@@@@#
58 | .@#&@@@@@%*,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,.,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,/&@@@@@%&@%((((#@@.
59 | .@%((((#@@@/#&@@@@&%#/*,.,..,,,,.,,,,,.,.,,,,,,,,,,,,,,,,..,.,..,,...,,,...,,,,,,.,,,,,,,,,,,../#%&@@@@@@@&%((///*********./(((/&&
60 | %@&%%#/***********./////(((((((####%%&&@@@@@@@@@@@@@@&@@@@@@@@@@@@@@@@&&%%%%%%%%#((((((((%@(((((#%@%/*******************./*/
61 |
62 | import {Owned} from "solmate/auth/Owned.sol";
63 | import {ERC721} from "solmate/tokens/ERC721.sol";
64 | import {LibString} from "solmate/utils/LibString.sol";
65 | import {MerkleProofLib} from "solmate/utils/MerkleProofLib.sol";
66 | import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol";
67 | import {ERC1155, ERC1155TokenReceiver} from "solmate/tokens/ERC1155.sol";
68 | import {toWadUnsafe, toDaysWadUnsafe} from "solmate/utils/SignedWadMath.sol";
69 |
70 | import {LibGOO} from "goo-issuance/LibGOO.sol";
71 | import {LogisticVRGDA} from "VRGDAs/LogisticVRGDA.sol";
72 |
73 | import {RandProvider} from "./utils/rand/RandProvider.sol";
74 | import {GobblersERC721} from "./utils/token/GobblersERC721.sol";
75 |
76 | import {Goo} from "./Goo.sol";
77 | import {Pages} from "./Pages.sol";
78 |
79 | /// @title Art Gobblers NFT
80 | /// @author FrankieIsLost
81 | /// @author transmissions11
82 | /// @notice An experimental decentralized art factory by Justin Roiland and Paradigm.
83 | contract ArtGobblers is GobblersERC721, LogisticVRGDA, Owned, ERC1155TokenReceiver {
84 | using LibString for uint256;
85 | using FixedPointMathLib for uint256;
86 |
87 | /*//////////////////////////////////////////////////////////////
88 | ADDRESSES
89 | //////////////////////////////////////////////////////////////*/
90 |
91 | /// @notice The address of the Goo ERC20 token contract.
92 | Goo public immutable goo;
93 |
94 | /// @notice The address of the Pages ERC721 token contract.
95 | Pages public immutable pages;
96 |
97 | /// @notice The address which receives gobblers reserved for the team.
98 | address public immutable team;
99 |
100 | /// @notice The address which receives gobblers reserved for the community.
101 | address public immutable community;
102 |
103 | /// @notice The address of a randomness provider. This provider will initially be
104 | /// a wrapper around Chainlink VRF v1, but can be changed in case it is fully sunset.
105 | RandProvider public randProvider;
106 |
107 | /*//////////////////////////////////////////////////////////////
108 | SUPPLY CONSTANTS
109 | //////////////////////////////////////////////////////////////*/
110 |
111 | /// @notice Maximum number of mintable gobblers.
112 | uint256 public constant MAX_SUPPLY = 10000;
113 |
114 | /// @notice Maximum amount of gobblers mintable via mintlist.
115 | uint256 public constant MINTLIST_SUPPLY = 2000;
116 |
117 | /// @notice Maximum amount of mintable legendary gobblers.
118 | uint256 public constant LEGENDARY_SUPPLY = 10;
119 |
120 | /// @notice Maximum amount of gobblers split between the reserves.
121 | /// @dev Set to comprise 20% of the sum of goo mintable gobblers + reserved gobblers.
122 | uint256 public constant RESERVED_SUPPLY = (MAX_SUPPLY - MINTLIST_SUPPLY - LEGENDARY_SUPPLY) / 5;
123 |
124 | /// @notice Maximum amount of gobblers that can be minted via VRGDA.
125 | // prettier-ignore
126 | uint256 public constant MAX_MINTABLE = MAX_SUPPLY
127 | - MINTLIST_SUPPLY
128 | - LEGENDARY_SUPPLY
129 | - RESERVED_SUPPLY;
130 |
131 | /*//////////////////////////////////////////////////////////////
132 | METADATA CONSTANTS
133 | //////////////////////////////////////////////////////////////*/
134 |
135 | /// @notice Provenance hash for gobbler metadata.
136 | bytes32 public immutable PROVENANCE_HASH;
137 |
138 | /// @notice URI for gobblers pending reveal.
139 | string public UNREVEALED_URI;
140 |
141 | /// @notice Base URI for minted gobblers.
142 | string public BASE_URI;
143 |
144 | /*//////////////////////////////////////////////////////////////
145 | MINTLIST STATE
146 | //////////////////////////////////////////////////////////////*/
147 |
148 | /// @notice Merkle root of mint mintlist.
149 | bytes32 public immutable merkleRoot;
150 |
151 | /// @notice Mapping to keep track of which addresses have claimed from mintlist.
152 | mapping(address => bool) public hasClaimedMintlistGobbler;
153 |
154 | /*//////////////////////////////////////////////////////////////
155 | VRGDA INPUT STATE
156 | //////////////////////////////////////////////////////////////*/
157 |
158 | /// @notice Timestamp for the start of minting.
159 | uint256 public immutable mintStart;
160 |
161 | /// @notice Number of gobblers minted from goo.
162 | uint128 public numMintedFromGoo;
163 |
164 | /*//////////////////////////////////////////////////////////////
165 | STANDARD GOBBLER STATE
166 | //////////////////////////////////////////////////////////////*/
167 |
168 | /// @notice Id of the most recently minted non legendary gobbler.
169 | /// @dev Will be 0 if no non legendary gobblers have been minted yet.
170 | uint128 public currentNonLegendaryId;
171 |
172 | /// @notice The number of gobblers minted to the reserves.
173 | uint256 public numMintedForReserves;
174 |
175 | /*//////////////////////////////////////////////////////////////
176 | LEGENDARY GOBBLER AUCTION STATE
177 | //////////////////////////////////////////////////////////////*/
178 |
179 | /// @notice Initial legendary gobbler auction price.
180 | uint256 public constant LEGENDARY_GOBBLER_INITIAL_START_PRICE = 69;
181 |
182 | /// @notice The last LEGENDARY_SUPPLY ids are reserved for legendary gobblers.
183 | uint256 public constant FIRST_LEGENDARY_GOBBLER_ID = MAX_SUPPLY - LEGENDARY_SUPPLY + 1;
184 |
185 | /// @notice Legendary auctions begin each time a multiple of these many gobblers have been minted from goo.
186 | /// @dev We add 1 to LEGENDARY_SUPPLY because legendary auctions begin only after the first interval.
187 | uint256 public constant LEGENDARY_AUCTION_INTERVAL = MAX_MINTABLE / (LEGENDARY_SUPPLY + 1);
188 |
189 | /// @notice Struct holding data required for legendary gobbler auctions.
190 | struct LegendaryGobblerAuctionData {
191 | // Start price of current legendary gobbler auction.
192 | uint128 startPrice;
193 | // Number of legendary gobblers sold so far.
194 | uint128 numSold;
195 | }
196 |
197 | /// @notice Data about the current legendary gobbler auction.
198 | LegendaryGobblerAuctionData public legendaryGobblerAuctionData;
199 |
200 | /*//////////////////////////////////////////////////////////////
201 | GOBBLER REVEAL STATE
202 | //////////////////////////////////////////////////////////////*/
203 |
204 | /// @notice Struct holding data required for gobbler reveals.
205 | struct GobblerRevealsData {
206 | // Last randomness obtained from the rand provider.
207 | uint64 randomSeed;
208 | // Next reveal cannot happen before this timestamp.
209 | uint64 nextRevealTimestamp;
210 | // Id of latest gobbler which has been revealed so far.
211 | uint64 lastRevealedId;
212 | // Remaining gobblers to be revealed with the current seed.
213 | uint56 toBeRevealed;
214 | // Whether we are waiting to receive a seed from the provider.
215 | bool waitingForSeed;
216 | }
217 |
218 | /// @notice Data about the current state of gobbler reveals.
219 | GobblerRevealsData public gobblerRevealsData;
220 |
221 | /*//////////////////////////////////////////////////////////////
222 | GOBBLED ART STATE
223 | //////////////////////////////////////////////////////////////*/
224 |
225 | /// @notice Maps gobbler ids to NFT contracts and their ids to the # of those NFT ids gobbled by the gobbler.
226 | mapping(uint256 => mapping(address => mapping(uint256 => uint256))) public getCopiesOfArtGobbledByGobbler;
227 |
228 | /*//////////////////////////////////////////////////////////////
229 | EVENTS
230 | //////////////////////////////////////////////////////////////*/
231 |
232 | event GooBalanceUpdated(address indexed user, uint256 newGooBalance);
233 |
234 | event GobblerClaimed(address indexed user, uint256 indexed gobblerId);
235 | event GobblerPurchased(address indexed user, uint256 indexed gobblerId, uint256 price);
236 | event LegendaryGobblerMinted(address indexed user, uint256 indexed gobblerId, uint256[] burnedGobblerIds);
237 | event ReservedGobblersMinted(address indexed user, uint256 lastMintedGobblerId, uint256 numGobblersEach);
238 |
239 | event RandomnessFulfilled(uint256 randomness);
240 | event RandomnessRequested(address indexed user, uint256 toBeRevealed);
241 | event RandProviderUpgraded(address indexed user, RandProvider indexed newRandProvider);
242 |
243 | event GobblersRevealed(address indexed user, uint256 numGobblers, uint256 lastRevealedId);
244 |
245 | event ArtGobbled(address indexed user, uint256 indexed gobblerId, address indexed nft, uint256 id);
246 |
247 | /*//////////////////////////////////////////////////////////////
248 | ERRORS
249 | //////////////////////////////////////////////////////////////*/
250 |
251 | error InvalidProof();
252 | error AlreadyClaimed();
253 | error MintStartPending();
254 |
255 | error SeedPending();
256 | error RevealsPending();
257 | error RequestTooEarly();
258 | error ZeroToBeRevealed();
259 | error NotRandProvider();
260 |
261 | error ReserveImbalance();
262 |
263 | error Cannibalism();
264 | error OwnerMismatch(address owner);
265 |
266 | error NoRemainingLegendaryGobblers();
267 | error CannotBurnLegendary(uint256 gobblerId);
268 | error InsufficientGobblerAmount(uint256 cost);
269 | error LegendaryAuctionNotStarted(uint256 gobblersLeft);
270 |
271 | error PriceExceededMax(uint256 currentPrice);
272 |
273 | error NotEnoughRemainingToBeRevealed(uint256 totalRemainingToBeRevealed);
274 |
275 | error UnauthorizedCaller(address caller);
276 |
277 | /*//////////////////////////////////////////////////////////////
278 | CONSTRUCTOR
279 | //////////////////////////////////////////////////////////////*/
280 |
281 | /// @notice Sets VRGDA parameters, mint config, relevant addresses, and URIs.
282 | /// @param _merkleRoot Merkle root of mint mintlist.
283 | /// @param _mintStart Timestamp for the start of the VRGDA mint.
284 | /// @param _goo Address of the Goo contract.
285 | /// @param _team Address of the team reserve.
286 | /// @param _community Address of the community reserve.
287 | /// @param _randProvider Address of the randomness provider.
288 | /// @param _baseUri Base URI for revealed gobblers.
289 | /// @param _unrevealedUri URI for unrevealed gobblers.
290 | /// @param _provenanceHash Provenance Hash for gobbler metadata.
291 | constructor(
292 | // Mint config:
293 | bytes32 _merkleRoot,
294 | uint256 _mintStart,
295 | // Addresses:
296 | Goo _goo,
297 | Pages _pages,
298 | address _team,
299 | address _community,
300 | RandProvider _randProvider,
301 | // URIs:
302 | string memory _baseUri,
303 | string memory _unrevealedUri,
304 | // Provenance:
305 | bytes32 _provenanceHash
306 | )
307 | GobblersERC721("Art Gobblers", "GOBBLER")
308 | Owned(msg.sender)
309 | LogisticVRGDA(
310 | 69.42e18, // Target price.
311 | 0.31e18, // Price decay percent.
312 | // Max gobblers mintable via VRGDA.
313 | toWadUnsafe(MAX_MINTABLE),
314 | 0.0023e18 // Time scale.
315 | )
316 | {
317 | mintStart = _mintStart;
318 | merkleRoot = _merkleRoot;
319 |
320 | goo = _goo;
321 | pages = _pages;
322 | team = _team;
323 | community = _community;
324 | randProvider = _randProvider;
325 |
326 | BASE_URI = _baseUri;
327 | UNREVEALED_URI = _unrevealedUri;
328 |
329 | PROVENANCE_HASH = _provenanceHash;
330 |
331 | // Set the starting price for the first legendary gobbler auction.
332 | legendaryGobblerAuctionData.startPrice = uint128(LEGENDARY_GOBBLER_INITIAL_START_PRICE);
333 |
334 | // Reveal for initial mint must wait a day from the start of the mint.
335 | gobblerRevealsData.nextRevealTimestamp = uint64(_mintStart + 1 days);
336 | }
337 |
338 | /*//////////////////////////////////////////////////////////////
339 | MINTLIST CLAIM LOGIC
340 | //////////////////////////////////////////////////////////////*/
341 |
342 | /// @notice Claim from mintlist, using a merkle proof.
343 | /// @dev Function does not directly enforce the MINTLIST_SUPPLY limit for gas efficiency. The
344 | /// limit is enforced during the creation of the merkle proof, which will be shared publicly.
345 | /// @param proof Merkle proof to verify the sender is mintlisted.
346 | /// @return gobblerId The id of the gobbler that was claimed.
347 | function claimGobbler(bytes32[] calldata proof) external returns (uint256 gobblerId) {
348 | // If minting has not yet begun, revert.
349 | if (mintStart > block.timestamp) revert MintStartPending();
350 |
351 | // If the user has already claimed, revert.
352 | if (hasClaimedMintlistGobbler[msg.sender]) revert AlreadyClaimed();
353 |
354 | // If the user's proof is invalid, revert.
355 | if (!MerkleProofLib.verify(proof, merkleRoot, keccak256(abi.encodePacked(msg.sender)))) revert InvalidProof();
356 |
357 | hasClaimedMintlistGobbler[msg.sender] = true;
358 |
359 | unchecked {
360 | // Overflow should be impossible due to supply cap of 10,000.
361 | emit GobblerClaimed(msg.sender, gobblerId = ++currentNonLegendaryId);
362 | }
363 |
364 | _mint(msg.sender, gobblerId);
365 | }
366 |
367 | /*//////////////////////////////////////////////////////////////
368 | MINTING LOGIC
369 | //////////////////////////////////////////////////////////////*/
370 |
371 | /// @notice Mint a gobbler, paying with goo.
372 | /// @param maxPrice Maximum price to pay to mint the gobbler.
373 | /// @param useVirtualBalance Whether the cost is paid from the
374 | /// user's virtual goo balance, or from their ERC20 goo balance.
375 | /// @return gobblerId The id of the gobbler that was minted.
376 | function mintFromGoo(uint256 maxPrice, bool useVirtualBalance) external returns (uint256 gobblerId) {
377 | // No need to check if we're at MAX_MINTABLE,
378 | // gobblerPrice() will revert once we reach it due to its
379 | // logistic nature. It will also revert prior to the mint start.
380 | uint256 currentPrice = gobblerPrice();
381 |
382 | // If the current price is above the user's specified max, revert.
383 | if (currentPrice > maxPrice) revert PriceExceededMax(currentPrice);
384 |
385 | // Decrement the user's goo balance by the current
386 | // price, either from virtual balance or ERC20 balance.
387 | useVirtualBalance
388 | ? updateUserGooBalance(msg.sender, currentPrice, GooBalanceUpdateType.DECREASE)
389 | : goo.burnForGobblers(msg.sender, currentPrice);
390 |
391 | unchecked {
392 | ++numMintedFromGoo; // Overflow should be impossible due to the supply cap.
393 |
394 | emit GobblerPurchased(msg.sender, gobblerId = ++currentNonLegendaryId, currentPrice);
395 | }
396 |
397 | _mint(msg.sender, gobblerId);
398 | }
399 |
400 | /// @notice Gobbler pricing in terms of goo.
401 | /// @dev Will revert if called before minting starts
402 | /// or after all gobblers have been minted via VRGDA.
403 | /// @return Current price of a gobbler in terms of goo.
404 | function gobblerPrice() public view returns (uint256) {
405 | // We need checked math here to cause underflow
406 | // before minting has begun, preventing mints.
407 | uint256 timeSinceStart = block.timestamp - mintStart;
408 |
409 | return getVRGDAPrice(toDaysWadUnsafe(timeSinceStart), numMintedFromGoo);
410 | }
411 |
412 | /*//////////////////////////////////////////////////////////////
413 | LEGENDARY GOBBLER AUCTION LOGIC
414 | //////////////////////////////////////////////////////////////*/
415 |
416 | /// @notice Mint a legendary gobbler by burning multiple standard gobblers.
417 | /// @param gobblerIds The ids of the standard gobblers to burn.
418 | /// @return gobblerId The id of the legendary gobbler that was minted.
419 | function mintLegendaryGobbler(uint256[] calldata gobblerIds) external returns (uint256 gobblerId) {
420 | // Get the number of legendary gobblers sold up until this point.
421 | uint256 numSold = legendaryGobblerAuctionData.numSold;
422 |
423 | gobblerId = FIRST_LEGENDARY_GOBBLER_ID + numSold; // Assign id.
424 |
425 | // This will revert if the auction hasn't started yet or legendaries
426 | // have sold out entirely, so there is no need to check here as well.
427 | uint256 cost = legendaryGobblerPrice();
428 |
429 | if (gobblerIds.length < cost) revert InsufficientGobblerAmount(cost);
430 |
431 | // Overflow should not occur in here, as most math is on emission multiples, which are inherently small.
432 | unchecked {
433 | uint256 burnedMultipleTotal; // The legendary's emissionMultiple will be 2x the sum of the gobblers burned.
434 |
435 | /*//////////////////////////////////////////////////////////////
436 | BATCH BURN LOGIC
437 | //////////////////////////////////////////////////////////////*/
438 |
439 | uint256 id; // Storing outside the loop saves ~7 gas per iteration.
440 |
441 | for (uint256 i = 0; i < cost; ++i) {
442 | id = gobblerIds[i];
443 |
444 | if (id >= FIRST_LEGENDARY_GOBBLER_ID) revert CannotBurnLegendary(id);
445 |
446 | GobblerData storage gobbler = getGobblerData[id];
447 |
448 | require(gobbler.owner == msg.sender, "WRONG_FROM");
449 |
450 | burnedMultipleTotal += gobbler.emissionMultiple;
451 |
452 | delete getApproved[id];
453 |
454 | emit Transfer(msg.sender, gobbler.owner = address(0), id);
455 | }
456 |
457 | /*//////////////////////////////////////////////////////////////
458 | LEGENDARY MINTING LOGIC
459 | //////////////////////////////////////////////////////////////*/
460 |
461 | // The legendary's emissionMultiple is 2x the sum of the multiples of the gobblers burned.
462 | getGobblerData[gobblerId].emissionMultiple = uint32(burnedMultipleTotal * 2);
463 |
464 | // Update the user's user data struct in one big batch. We add burnedMultipleTotal to their
465 | // emission multiple (not burnedMultipleTotal * 2) to account for the standard gobblers that
466 | // were burned and hence should have their multiples subtracted from the user's total multiple.
467 | getUserData[msg.sender].lastBalance = uint128(gooBalance(msg.sender)); // Checkpoint balance.
468 | getUserData[msg.sender].lastTimestamp = uint64(block.timestamp); // Store time alongside it.
469 | getUserData[msg.sender].emissionMultiple += uint32(burnedMultipleTotal); // Update multiple.
470 | // Update the total number of gobblers owned by the user. The call to _mint
471 | // below will increase the count by 1 to account for the new legendary gobbler.
472 | getUserData[msg.sender].gobblersOwned -= uint32(cost);
473 |
474 | // New start price is the max of LEGENDARY_GOBBLER_INITIAL_START_PRICE and cost * 2.
475 | legendaryGobblerAuctionData.startPrice = uint128(
476 | cost <= LEGENDARY_GOBBLER_INITIAL_START_PRICE / 2 ? LEGENDARY_GOBBLER_INITIAL_START_PRICE : cost * 2
477 | );
478 | legendaryGobblerAuctionData.numSold = uint128(numSold + 1); // Increment the # of legendaries sold.
479 |
480 | // If gobblerIds has 1,000 elements this should cost around ~270,000 gas.
481 | emit LegendaryGobblerMinted(msg.sender, gobblerId, gobblerIds[:cost]);
482 |
483 | _mint(msg.sender, gobblerId);
484 | }
485 | }
486 |
487 | /// @notice Calculate the legendary gobbler price in terms of gobblers, according to a linear decay function.
488 | /// @dev The price of a legendary gobbler decays as gobblers are minted. The first legendary auction begins when
489 | /// 1 LEGENDARY_AUCTION_INTERVAL worth of gobblers are minted, and the price decays linearly while the next interval of
490 | /// gobblers are minted. Every time an additional interval is minted, a new auction begins until all legendaries have been sold.
491 | /// @dev Will revert if the auction hasn't started yet or legendaries have sold out entirely.
492 | /// @return The current price of the legendary gobbler being auctioned, in terms of gobblers.
493 | function legendaryGobblerPrice() public view returns (uint256) {
494 | // Retrieve and cache various auction parameters and variables.
495 | uint256 startPrice = legendaryGobblerAuctionData.startPrice;
496 | uint256 numSold = legendaryGobblerAuctionData.numSold;
497 |
498 | // If all legendary gobblers have been sold, there are none left to auction.
499 | if (numSold == LEGENDARY_SUPPLY) revert NoRemainingLegendaryGobblers();
500 |
501 | unchecked {
502 | // Get and cache the number of standard gobblers sold via VRGDA up until this point.
503 | uint256 mintedFromGoo = numMintedFromGoo;
504 |
505 | // The number of gobblers minted at the start of the auction is computed by multiplying the # of
506 | // intervals that must pass before the next auction begins by the number of gobblers in each interval.
507 | uint256 numMintedAtStart = (numSold + 1) * LEGENDARY_AUCTION_INTERVAL;
508 |
509 | // If not enough gobblers have been minted to start the auction yet, return how many need to be minted.
510 | if (numMintedAtStart > mintedFromGoo) revert LegendaryAuctionNotStarted(numMintedAtStart - mintedFromGoo);
511 |
512 | // Compute how many gobblers were minted since the auction began.
513 | uint256 numMintedSinceStart = mintedFromGoo - numMintedAtStart;
514 |
515 | // prettier-ignore
516 | // If we've minted the full interval or beyond it, the price has decayed to 0.
517 | if (numMintedSinceStart >= LEGENDARY_AUCTION_INTERVAL) return 0;
518 | // Otherwise decay the price linearly based on what fraction of the interval has been minted.
519 | else return FixedPointMathLib.unsafeDivUp(startPrice * (LEGENDARY_AUCTION_INTERVAL - numMintedSinceStart), LEGENDARY_AUCTION_INTERVAL);
520 | }
521 | }
522 |
523 | /*//////////////////////////////////////////////////////////////
524 | RANDOMNESS LOGIC
525 | //////////////////////////////////////////////////////////////*/
526 |
527 | /// @notice Request a new random seed for revealing gobblers.
528 | function requestRandomSeed() external returns (bytes32) {
529 | uint256 nextRevealTimestamp = gobblerRevealsData.nextRevealTimestamp;
530 |
531 | // A new random seed cannot be requested before the next reveal timestamp.
532 | if (block.timestamp < nextRevealTimestamp) revert RequestTooEarly();
533 |
534 | // A random seed can only be requested when all gobblers from the previous seed have been revealed.
535 | // This prevents a user from requesting additional randomness in hopes of a more favorable outcome.
536 | if (gobblerRevealsData.toBeRevealed != 0) revert RevealsPending();
537 |
538 | unchecked {
539 | // Prevent revealing while we wait for the seed.
540 | gobblerRevealsData.waitingForSeed = true;
541 |
542 | // Compute the number of gobblers to be revealed with the seed.
543 | uint256 toBeRevealed = currentNonLegendaryId - gobblerRevealsData.lastRevealedId;
544 |
545 | // Ensure that there are more than 0 gobblers to be revealed,
546 | // otherwise the contract could waste LINK revealing nothing.
547 | if (toBeRevealed == 0) revert ZeroToBeRevealed();
548 |
549 | // Lock in the number of gobblers to be revealed from seed.
550 | gobblerRevealsData.toBeRevealed = uint56(toBeRevealed);
551 |
552 | // We enable reveals for a set of gobblers every 24 hours.
553 | // Timestamp overflow is impossible on human timescales.
554 | gobblerRevealsData.nextRevealTimestamp = uint64(nextRevealTimestamp + 1 days);
555 |
556 | emit RandomnessRequested(msg.sender, toBeRevealed);
557 | }
558 |
559 | // Call out to the randomness provider.
560 | return randProvider.requestRandomBytes();
561 | }
562 |
563 | /// @notice Callback from rand provider. Sets randomSeed. Can only be called by the rand provider.
564 | /// @param randomness The 256 bits of verifiable randomness provided by the rand provider.
565 | function acceptRandomSeed(bytes32, uint256 randomness) external {
566 | // The caller must be the randomness provider, revert in the case it's not.
567 | if (msg.sender != address(randProvider)) revert NotRandProvider();
568 |
569 | // The unchecked cast to uint64 is equivalent to moduloing the randomness by 2**64.
570 | gobblerRevealsData.randomSeed = uint64(randomness); // 64 bits of randomness is plenty.
571 |
572 | gobblerRevealsData.waitingForSeed = false; // We have the seed now, open up reveals.
573 |
574 | emit RandomnessFulfilled(randomness);
575 | }
576 |
577 | /// @notice Upgrade the rand provider contract. Useful if current VRF is sunset.
578 | /// @param newRandProvider The new randomness provider contract address.
579 | function upgradeRandProvider(RandProvider newRandProvider) external onlyOwner {
580 | // Reset reveal state when we upgrade while the seed is pending. This gives us a
581 | // safeguard against malfunctions since we won't be stuck waiting for a seed forever.
582 | if (gobblerRevealsData.waitingForSeed) {
583 | gobblerRevealsData.waitingForSeed = false;
584 | gobblerRevealsData.toBeRevealed = 0;
585 | gobblerRevealsData.nextRevealTimestamp -= 1 days;
586 | }
587 |
588 | randProvider = newRandProvider; // Update the randomness provider.
589 |
590 | emit RandProviderUpgraded(msg.sender, newRandProvider);
591 | }
592 |
593 | /*//////////////////////////////////////////////////////////////
594 | GOBBLER REVEAL LOGIC
595 | //////////////////////////////////////////////////////////////*/
596 |
597 | /// @notice Knuth shuffle to progressively reveal
598 | /// new gobblers using entropy from a random seed.
599 | /// @param numGobblers The number of gobblers to reveal.
600 | function revealGobblers(uint256 numGobblers) external {
601 | uint256 randomSeed = gobblerRevealsData.randomSeed;
602 |
603 | uint256 lastRevealedId = gobblerRevealsData.lastRevealedId;
604 |
605 | uint256 totalRemainingToBeRevealed = gobblerRevealsData.toBeRevealed;
606 |
607 | // Can't reveal if we're still waiting for a new seed.
608 | if (gobblerRevealsData.waitingForSeed) revert SeedPending();
609 |
610 | // Can't reveal more gobblers than are currently remaining to be revealed with the seed.
611 | if (numGobblers > totalRemainingToBeRevealed) revert NotEnoughRemainingToBeRevealed(totalRemainingToBeRevealed);
612 |
613 | // Implements a Knuth shuffle. If something in
614 | // here can overflow, we've got bigger problems.
615 | unchecked {
616 | for (uint256 i = 0; i < numGobblers; ++i) {
617 | /*//////////////////////////////////////////////////////////////
618 | DETERMINE RANDOM SWAP
619 | //////////////////////////////////////////////////////////////*/
620 |
621 | // Number of ids that have not been revealed. Subtract 1
622 | // because we don't want to include any legendaries in the swap.
623 | uint256 remainingIds = FIRST_LEGENDARY_GOBBLER_ID - lastRevealedId - 1;
624 |
625 | // Randomly pick distance for swap.
626 | uint256 distance = randomSeed % remainingIds;
627 |
628 | // Current id is consecutive to last reveal.
629 | uint256 currentId = ++lastRevealedId;
630 |
631 | // Select swap id, adding distance to next reveal id.
632 | uint256 swapId = currentId + distance;
633 |
634 | /*//////////////////////////////////////////////////////////////
635 | GET INDICES FOR IDS
636 | //////////////////////////////////////////////////////////////*/
637 |
638 | // Get the index of the swap id.
639 | uint64 swapIndex = getGobblerData[swapId].idx == 0
640 | ? uint64(swapId) // Hasn't been shuffled before.
641 | : getGobblerData[swapId].idx; // Shuffled before.
642 |
643 | // Get the owner of the current id.
644 | address currentIdOwner = getGobblerData[currentId].owner;
645 |
646 | // Get the index of the current id.
647 | uint64 currentIndex = getGobblerData[currentId].idx == 0
648 | ? uint64(currentId) // Hasn't been shuffled before.
649 | : getGobblerData[currentId].idx; // Shuffled before.
650 |
651 | /*//////////////////////////////////////////////////////////////
652 | SWAP INDICES AND SET MULTIPLE
653 | //////////////////////////////////////////////////////////////*/
654 |
655 | // Determine the current id's new emission multiple.
656 | uint256 newCurrentIdMultiple = 9; // For beyond 7963.
657 |
658 | // The branchless expression below is equivalent to:
659 | // if (swapIndex <= 3054) newCurrentIdMultiple = 6;
660 | // else if (swapIndex <= 5672) newCurrentIdMultiple = 7;
661 | // else if (swapIndex <= 7963) newCurrentIdMultiple = 8;
662 | assembly {
663 | // prettier-ignore
664 | newCurrentIdMultiple := sub(sub(sub(
665 | newCurrentIdMultiple,
666 | lt(swapIndex, 7964)),
667 | lt(swapIndex, 5673)),
668 | lt(swapIndex, 3055)
669 | )
670 | }
671 |
672 | // Swap the index and multiple of the current id.
673 | getGobblerData[currentId].idx = swapIndex;
674 | getGobblerData[currentId].emissionMultiple = uint32(newCurrentIdMultiple);
675 |
676 | // Swap the index of the swap id.
677 | getGobblerData[swapId].idx = currentIndex;
678 |
679 | /*//////////////////////////////////////////////////////////////
680 | UPDATE CURRENT ID MULTIPLE
681 | //////////////////////////////////////////////////////////////*/
682 |
683 | // Update the user data for the owner of the current id.
684 | getUserData[currentIdOwner].lastBalance = uint128(gooBalance(currentIdOwner));
685 | getUserData[currentIdOwner].lastTimestamp = uint64(block.timestamp);
686 | getUserData[currentIdOwner].emissionMultiple += uint32(newCurrentIdMultiple);
687 |
688 | // Update the random seed to choose a new distance for the next iteration.
689 | // It is critical that we cast to uint64 here, as otherwise the random seed
690 | // set after calling revealGobblers(1) thrice would differ from the seed set
691 | // after calling revealGobblers(3) a single time. This would enable an attacker
692 | // to choose from a number of different seeds and use whichever is most favorable.
693 | // Equivalent to randomSeed = uint64(uint256(keccak256(abi.encodePacked(randomSeed))))
694 | assembly {
695 | mstore(0, randomSeed) // Store the random seed in scratch space.
696 |
697 | // Moduloing by 2 ** 64 is equivalent to a uint64 cast.
698 | randomSeed := mod(keccak256(0, 32), exp(2, 64))
699 | }
700 | }
701 |
702 | // Update all relevant reveal state.
703 | gobblerRevealsData.randomSeed = uint64(randomSeed);
704 | gobblerRevealsData.lastRevealedId = uint64(lastRevealedId);
705 | gobblerRevealsData.toBeRevealed = uint56(totalRemainingToBeRevealed - numGobblers);
706 |
707 | emit GobblersRevealed(msg.sender, numGobblers, lastRevealedId);
708 | }
709 | }
710 |
711 | /*//////////////////////////////////////////////////////////////
712 | URI LOGIC
713 | //////////////////////////////////////////////////////////////*/
714 |
715 | /// @notice Returns a token's URI if it has been minted.
716 | /// @param gobblerId The id of the token to get the URI for.
717 | function tokenURI(uint256 gobblerId) public view virtual override returns (string memory) {
718 | // Between 0 and lastRevealed are revealed normal gobblers.
719 | if (gobblerId <= gobblerRevealsData.lastRevealedId) {
720 | if (gobblerId == 0) revert("NOT_MINTED"); // 0 is not a valid id for Art Gobblers.
721 |
722 | return string.concat(BASE_URI, uint256(getGobblerData[gobblerId].idx).toString());
723 | }
724 |
725 | // Between lastRevealed + 1 and currentNonLegendaryId are minted but not revealed.
726 | if (gobblerId <= currentNonLegendaryId) return UNREVEALED_URI;
727 |
728 | // Between currentNonLegendaryId and FIRST_LEGENDARY_GOBBLER_ID are unminted.
729 | if (gobblerId < FIRST_LEGENDARY_GOBBLER_ID) revert("NOT_MINTED");
730 |
731 | // Between FIRST_LEGENDARY_GOBBLER_ID and FIRST_LEGENDARY_GOBBLER_ID + numSold are minted legendaries.
732 | if (gobblerId < FIRST_LEGENDARY_GOBBLER_ID + legendaryGobblerAuctionData.numSold)
733 | return string.concat(BASE_URI, gobblerId.toString());
734 |
735 | revert("NOT_MINTED"); // Unminted legendaries and invalid token ids.
736 | }
737 |
738 | /*//////////////////////////////////////////////////////////////
739 | GOBBLE ART LOGIC
740 | //////////////////////////////////////////////////////////////*/
741 |
742 | /// @notice Feed a gobbler a work of art.
743 | /// @param gobblerId The gobbler to feed the work of art.
744 | /// @param nft The ERC721 or ERC1155 contract of the work of art.
745 | /// @param id The id of the work of art.
746 | /// @param isERC1155 Whether the work of art is an ERC1155 token.
747 | function gobble(
748 | uint256 gobblerId,
749 | address nft,
750 | uint256 id,
751 | bool isERC1155
752 | ) external {
753 | // Get the owner of the gobbler to feed.
754 | address owner = getGobblerData[gobblerId].owner;
755 |
756 | // The caller must own the gobbler they're feeding.
757 | if (owner != msg.sender) revert OwnerMismatch(owner);
758 |
759 | // Gobblers have taken a vow not to eat other gobblers.
760 | if (nft == address(this)) revert Cannibalism();
761 |
762 | unchecked {
763 | // Increment the # of copies gobbled by the gobbler. Unchecked is
764 | // safe, as an NFT can't have more than type(uint256).max copies.
765 | ++getCopiesOfArtGobbledByGobbler[gobblerId][nft][id];
766 | }
767 |
768 | emit ArtGobbled(msg.sender, gobblerId, nft, id);
769 |
770 | isERC1155
771 | ? ERC1155(nft).safeTransferFrom(msg.sender, address(this), id, 1, "")
772 | : ERC721(nft).transferFrom(msg.sender, address(this), id);
773 | }
774 |
775 | /*//////////////////////////////////////////////////////////////
776 | GOO LOGIC
777 | //////////////////////////////////////////////////////////////*/
778 |
779 | /// @notice Calculate a user's virtual goo balance.
780 | /// @param user The user to query balance for.
781 | function gooBalance(address user) public view returns (uint256) {
782 | // Compute the user's virtual goo balance using LibGOO.
783 | // prettier-ignore
784 | return LibGOO.computeGOOBalance(
785 | getUserData[user].emissionMultiple,
786 | getUserData[user].lastBalance,
787 | uint256(toDaysWadUnsafe(block.timestamp - getUserData[user].lastTimestamp))
788 | );
789 | }
790 |
791 | /// @notice Add goo to your emission balance,
792 | /// burning the corresponding ERC20 balance.
793 | /// @param gooAmount The amount of goo to add.
794 | function addGoo(uint256 gooAmount) external {
795 | // Burn goo being added to gobbler.
796 | goo.burnForGobblers(msg.sender, gooAmount);
797 |
798 | // Increase msg.sender's virtual goo balance.
799 | updateUserGooBalance(msg.sender, gooAmount, GooBalanceUpdateType.INCREASE);
800 | }
801 |
802 | /// @notice Remove goo from your emission balance, and
803 | /// add the corresponding amount to your ERC20 balance.
804 | /// @param gooAmount The amount of goo to remove.
805 | function removeGoo(uint256 gooAmount) external {
806 | // Decrease msg.sender's virtual goo balance.
807 | updateUserGooBalance(msg.sender, gooAmount, GooBalanceUpdateType.DECREASE);
808 |
809 | // Mint the corresponding amount of ERC20 goo.
810 | goo.mintForGobblers(msg.sender, gooAmount);
811 | }
812 |
813 | /// @notice Burn an amount of a user's virtual goo balance. Only callable
814 | /// by the Pages contract to enable purchasing pages with virtual balance.
815 | /// @param user The user whose virtual goo balance we should burn from.
816 | /// @param gooAmount The amount of goo to burn from the user's virtual balance.
817 | function burnGooForPages(address user, uint256 gooAmount) external {
818 | // The caller must be the Pages contract, revert otherwise.
819 | if (msg.sender != address(pages)) revert UnauthorizedCaller(msg.sender);
820 |
821 | // Burn the requested amount of goo from the user's virtual goo balance.
822 | // Will revert if the user doesn't have enough goo in their virtual balance.
823 | updateUserGooBalance(user, gooAmount, GooBalanceUpdateType.DECREASE);
824 | }
825 |
826 | /// @dev An enum for representing whether to
827 | /// increase or decrease a user's goo balance.
828 | enum GooBalanceUpdateType {
829 | INCREASE,
830 | DECREASE
831 | }
832 |
833 | /// @notice Update a user's virtual goo balance.
834 | /// @param user The user whose virtual goo balance we should update.
835 | /// @param gooAmount The amount of goo to update the user's virtual balance by.
836 | /// @param updateType Whether to increase or decrease the user's balance by gooAmount.
837 | function updateUserGooBalance(
838 | address user,
839 | uint256 gooAmount,
840 | GooBalanceUpdateType updateType
841 | ) internal {
842 | // Will revert due to underflow if we're decreasing by more than the user's current balance.
843 | // Don't need to do checked addition in the increase case, but we do it anyway for convenience.
844 | uint256 updatedBalance = updateType == GooBalanceUpdateType.INCREASE
845 | ? gooBalance(user) + gooAmount
846 | : gooBalance(user) - gooAmount;
847 |
848 | // Snapshot the user's new goo balance with the current timestamp.
849 | getUserData[user].lastBalance = uint128(updatedBalance);
850 | getUserData[user].lastTimestamp = uint64(block.timestamp);
851 |
852 | emit GooBalanceUpdated(user, updatedBalance);
853 | }
854 |
855 | /*//////////////////////////////////////////////////////////////
856 | RESERVED GOBBLERS MINTING LOGIC
857 | //////////////////////////////////////////////////////////////*/
858 |
859 | /// @notice Mint a number of gobblers to the reserves.
860 | /// @param numGobblersEach The number of gobblers to mint to each reserve.
861 | /// @dev Gobblers minted to reserves cannot comprise more than 20% of the sum of
862 | /// the supply of goo minted gobblers and the supply of gobblers minted to reserves.
863 | function mintReservedGobblers(uint256 numGobblersEach) external returns (uint256 lastMintedGobblerId) {
864 | unchecked {
865 | // Optimistically increment numMintedForReserves, may be reverted below.
866 | // Overflow in this calculation is possible but numGobblersEach would have to
867 | // be so large that it would cause the loop in _batchMint to run out of gas quickly.
868 | uint256 newNumMintedForReserves = numMintedForReserves += (numGobblersEach * 2);
869 |
870 | // Ensure that after this mint gobblers minted to reserves won't comprise more than 20% of
871 | // the sum of the supply of goo minted gobblers and the supply of gobblers minted to reserves.
872 | if (newNumMintedForReserves > (numMintedFromGoo + newNumMintedForReserves) / 5) revert ReserveImbalance();
873 | }
874 |
875 | // Mint numGobblersEach gobblers to both the team and community reserve.
876 | lastMintedGobblerId = _batchMint(team, numGobblersEach, currentNonLegendaryId);
877 | lastMintedGobblerId = _batchMint(community, numGobblersEach, lastMintedGobblerId);
878 |
879 | currentNonLegendaryId = uint128(lastMintedGobblerId); // Set currentNonLegendaryId.
880 |
881 | emit ReservedGobblersMinted(msg.sender, lastMintedGobblerId, numGobblersEach);
882 | }
883 |
884 | /*//////////////////////////////////////////////////////////////
885 | CONVENIENCE FUNCTIONS
886 | //////////////////////////////////////////////////////////////*/
887 |
888 | /// @notice Convenience function to get emissionMultiple for a gobbler.
889 | /// @param gobblerId The gobbler to get emissionMultiple for.
890 | function getGobblerEmissionMultiple(uint256 gobblerId) external view returns (uint256) {
891 | return getGobblerData[gobblerId].emissionMultiple;
892 | }
893 |
894 | /// @notice Convenience function to get emissionMultiple for a user.
895 | /// @param user The user to get emissionMultiple for.
896 | function getUserEmissionMultiple(address user) external view returns (uint256) {
897 | return getUserData[user].emissionMultiple;
898 | }
899 |
900 | /*//////////////////////////////////////////////////////////////
901 | ERC721 LOGIC
902 | //////////////////////////////////////////////////////////////*/
903 |
904 | function transferFrom(
905 | address from,
906 | address to,
907 | uint256 id
908 | ) public override {
909 | require(from == getGobblerData[id].owner, "WRONG_FROM");
910 |
911 | require(to != address(0), "INVALID_RECIPIENT");
912 |
913 | require(
914 | msg.sender == from || isApprovedForAll[from][msg.sender] || msg.sender == getApproved[id],
915 | "NOT_AUTHORIZED"
916 | );
917 |
918 | delete getApproved[id];
919 |
920 | getGobblerData[id].owner = to;
921 |
922 | unchecked {
923 | uint32 emissionMultiple = getGobblerData[id].emissionMultiple; // Caching saves gas.
924 |
925 | // We update their last balance before updating their emission multiple to avoid
926 | // penalizing them by retroactively applying their new (lower) emission multiple.
927 | getUserData[from].lastBalance = uint128(gooBalance(from));
928 | getUserData[from].lastTimestamp = uint64(block.timestamp);
929 | getUserData[from].emissionMultiple -= emissionMultiple;
930 | getUserData[from].gobblersOwned -= 1;
931 |
932 | // We update their last balance before updating their emission multiple to avoid
933 | // overpaying them by retroactively applying their new (higher) emission multiple.
934 | getUserData[to].lastBalance = uint128(gooBalance(to));
935 | getUserData[to].lastTimestamp = uint64(block.timestamp);
936 | getUserData[to].emissionMultiple += emissionMultiple;
937 | getUserData[to].gobblersOwned += 1;
938 | }
939 |
940 | emit Transfer(from, to, id);
941 | }
942 | }
943 |
--------------------------------------------------------------------------------
/src/Goo.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {ERC20} from "solmate/tokens/ERC20.sol";
5 |
6 | /* %#/*********(&,
7 | .#*********************#.
8 | #****./*********************%
9 | %*******************************%
10 | &**********************************,((
11 | @(*,***********************************#&
12 | (*********************#***********************(
13 | ,%@/**************#%***%**&***%*******************,
14 | /********************#****#*#******,**************%
15 | ,************,#(*****************(#/&(*,*,*********#
16 | **************(%%(&************#@%(///************(
17 | ./**************,*./##****************************#*%
18 | #**&**************************************************&@@@@@&@&%#((./.
19 | (*******************@&%&@@@. / % &********(@/,****,,,*,,,****,,*,**********,*,
20 | &******************# / * / / .. %/****(******************,**&***********./
21 | /%(*******************&***./# #.#%% ., ., ##&&@****#***********************************.
22 | *#(*,**************************(***(///.* * # # . %*****(/*************************************&
23 | *(***********************************.//****& # # (#&((%@*,*&(******(%************./@#* *%&%(/&*************(
24 | #,**************************************,&******&..*#&(*****,,,,/******************************** (/******,**,**,
25 | %*****************************************.//**************#************************************** .(***********#
26 | (*************************./************************************************************************ @**************
27 | ,**********&@@@&&%# &,**********************************************************************@ ./*,%*,********./
28 | *********** .************@(*************(///////////////.//#&%/*****************&*,, &************%
29 | (**********. .%********************(&./////////////////////////////(%****************** *(**&,#*
30 | #**********(, &,*./***************%(///////////////////////////////////*&****************
31 | (************% %,*****************&///////////////////////////////////////*(***************.
32 | .(***************( #******************&//////////////////////////////////////////****************
33 | .&*************%*./ .*******************%/////////////////////////////////////////****************##
34 | .*************%*% (********************#(///////////////////////////////////(#*****************&**,***,.
35 | #***./,***% #**********************,%%*./////////////////////////*(@*******************(/****./********,((
36 | @@, &**@*****************************./(%@&%%((((((%&&%(*********************************&,**********.
37 | . .#,,*****./&/*****************************************************************************************
38 | %,******************************************************************************************************#
39 | %*******@*****************************************************./#%%,...((, .,********************(
40 | ,*******************************@&(**./%&%* .,//(//////////, ,************./
41 | /**************************&* ////*(///////// ***(*********%
42 | (*********************(# ..///////////(//( .***********./
43 | #******************% *..,,,(//////////(//(*.//, %***************&
44 | %***************** ////////&&&&&&&&%#(//(&@(#@@ &*********************#
45 | #****************. ,//(//////(@@%%%%%///////****& &************************(
46 | .**&***(************./ .@.,(///(/(.//(***((*(//*****@/& ,*************************./
47 | &********************# .(#(@#//(****(//(*****(/(&(..&( ./*********************(#.
48 | #/***********************./ /,,./*((#%@(%&%(((((((#%&&&/(#(#@(
49 | #*,***********************,*& .%@@@, ///(/*
50 | (*************************% ..(/,./(,.,*
51 | /#/*./(%&(.*/
52 |
53 | /// @title Goo Token (GOO)
54 | /// @author FrankieIsLost
55 | /// @author transmissions11
56 | /// @notice Goo is the in-game token for ArtGobblers. It's a standard ERC20
57 | /// token that can be burned and minted by the gobblers and pages contract.
58 | contract Goo is ERC20("Goo", "GOO", 18) {
59 | /*//////////////////////////////////////////////////////////////
60 | ADDRESSES
61 | //////////////////////////////////////////////////////////////*/
62 |
63 | /// @notice The address of the Art Gobblers contract.
64 | address public immutable artGobblers;
65 |
66 | /// @notice The address of the Pages contract.
67 | address public immutable pages;
68 |
69 | /*//////////////////////////////////////////////////////////////
70 | ERRORS
71 | //////////////////////////////////////////////////////////////*/
72 |
73 | error Unauthorized();
74 |
75 | /*//////////////////////////////////////////////////////////////
76 | CONSTRUCTOR
77 | //////////////////////////////////////////////////////////////*/
78 |
79 | /// @notice Sets the addresses of relevant contracts.
80 | /// @param _artGobblers Address of the ArtGobblers contract.
81 | /// @param _pages Address of the Pages contract.
82 | constructor(address _artGobblers, address _pages) {
83 | artGobblers = _artGobblers;
84 | pages = _pages;
85 | }
86 |
87 | /*//////////////////////////////////////////////////////////////
88 | MINT/BURN LOGIC
89 | //////////////////////////////////////////////////////////////*/
90 |
91 | /// @notice Requires caller address to match user address.
92 | modifier only(address user) {
93 | if (msg.sender != user) revert Unauthorized();
94 |
95 | _;
96 | }
97 |
98 | /// @notice Mint any amount of goo to a user. Can only be called by ArtGobblers.
99 | /// @param to The address of the user to mint goo to.
100 | /// @param amount The amount of goo to mint.
101 | function mintForGobblers(address to, uint256 amount) external only(artGobblers) {
102 | _mint(to, amount);
103 | }
104 |
105 | /// @notice Burn any amount of goo from a user. Can only be called by ArtGobblers.
106 | /// @param from The address of the user to burn goo from.
107 | /// @param amount The amount of goo to burn.
108 | function burnForGobblers(address from, uint256 amount) external only(artGobblers) {
109 | _burn(from, amount);
110 | }
111 |
112 | /// @notice Burn any amount of goo from a user. Can only be called by Pages.
113 | /// @param from The address of the user to burn goo from.
114 | /// @param amount The amount of goo to burn.
115 | function burnForPages(address from, uint256 amount) external only(pages) {
116 | _burn(from, amount);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Pages.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {LibString} from "solmate/utils/LibString.sol";
5 | import {toDaysWadUnsafe} from "solmate/utils/SignedWadMath.sol";
6 |
7 | import {LogisticToLinearVRGDA} from "VRGDAs/LogisticToLinearVRGDA.sol";
8 |
9 | import {PagesERC721} from "./utils/token/PagesERC721.sol";
10 |
11 | import {Goo} from "./Goo.sol";
12 | import {ArtGobblers} from "./ArtGobblers.sol";
13 |
14 | /* &@./(
15 | &//*&
16 | @/*.&
17 | (#/./%
18 | .,(#%%&@@&%#(/, *#./#, .*(%@@@@&%#((//////(#&@%.
19 | #&@&/*./*************.///&@%&@@@@@@@@&%#(///**********./********************#&
20 | &@(/*****************************************************************************#/
21 | (&**********************************************************************************@
22 | @**********************************************************************************(&
23 | &/*******************************************************************************%@.
24 | ,( *&/***********************************************************************./#@@%(((#@%
25 | .// (@/***./*****************************************************.///#&@@@#(((#(##((##%&
26 | /*.// *@@(/////(#&@@@@@@@@@@@@@@@@@@@@@@@@@@@&&&&%%%%%####(#(((((((##(((##((#(###((((#@&
27 | (****./ @###(###(((((###(##((#(((((#(#####((((#(((((((#((((((((((((((((((((#(##(####((((#(#@
28 | /******./ ((#########(############((#(######(########(####(##(###%&&&%########%&&%###((((####@
29 | ,********./ /&((((#(#((##(((#(((#((########(###((((((((((((((((#((%((#((#((((((((((#((((((((((((#@
30 | .**********( @#(#(#####(###(#&&(((((((((#(((%@%##########(######((#(((##((((%&@%#((##(#(((#((#%&
31 | /**********( /%(#(((((#((#&%(((#(###(#########(((###(((#####(###((#(%@%###(###((((##(((##&%#(##(((##(#@,
32 | (**********./ @#(#(((((###((#(#((#(((((###(((#(#%@@###((#####(###((#&%&&/. .*#&&@#((((((((%@
33 | ./**********( ,%((((((((##((((#&@#(##((((((##%%&&&&%%##((###(##((%* .@#(##((##@.
34 | ./********./ %#((#(((((#####@##(##%@&(. %#((#((###%* *(,. %#((#(((@,
35 | /********( @#((#((((####@#%@# .&((((##(& &@@@@@@% ((#&,
36 | /******( ,&(#(#((((###@& .#(###& /@@@@@@ (%((((#@%
37 | ./*#@@@@* (%#(((((#((#& #@@@@@&. (%#(#((&. /#. ##(#(#(##@&
38 | @%(/((((& ###(((((#@ .@@@@@@@ &((##(#@@(. .#&%#&(#####((##@.
39 | /#(((((((## (#* @##(#(((((#@ ,@@@/* @&((####(&(##%%&&&%%##%&&&&&%##(((#%%(###((####((&&
40 | #((((%@@@@@. @#(((#@*#(((((((#@ .%@#((####(((#((#(((((((((((((((((((##%((#(###((#####(#@
41 | #@/,,,@#((#%( @((&%,,,&&%((((((#(#&, .%@%#((#@%(((###((((#(((##%&&&&&&&&&&&%#((((####(###(###(###((#@%
42 | #@##&(,,,(&(##(#@ ##@**#@@@@(((((((##(@@@#, ,(@@#(((((((((#((((#####((#%@@@@@@@@@@@@@@@@@&&%%################%%(##@&
43 | %#((((#&@@%(((((%# #(####(#&@&(((((((#(##@#((((((((##((###(#(#(((#%#(#####(####(##@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&%##(((#((%@
44 | *((#######((((###(%&@ (#(####(((#&@%(((((####(##%#(#((((###(###%@@%###(##%%%###(#(####@@@@&%###((##((((((((##((((((((##((((((#((##(#@
45 | (&((((((((((((((((##@&/((@@###(((((((#&&%#(#((((((((#(((((#((###(#((#&@&&@@@@@@@@%((((((#((((#((((((((((((((((((((((((((((((((((((((((((#
46 | .@#(##((((((((((#((&@#%@@&%%&@#(((###(#####(#@%##(###((#######(((#(#%@#(((#((#(######(###############(###############(###############(##(##
47 | (%(#((#((((((((((#&@%#(%%,,,,(#@######(####(((((%%(#(#(###(#(((#%@@@@@#(####(((#(#(#(#######(#######(#######(#######(#######(#######(##((#
48 | (%((####((###(#&@########@*,,,@#(#%@(((#((#######(((#&(##(###(%@@@@@%##((#@&%####&@@%(###(###############(###############(###############(###(#
49 | @((##(##(((#@%#(#((##(#(#(#&@(##@&&@@@#(#(###(#((#%&@@%##(#(#(((####((((&%((###((####(###(###(###(###(###(###(###(###(###(###(###(####(
50 | @((#(((##@%(((((((######((###(#&@%#(#(#(#(##########(%%((((#((((#(((((((((((((#&(###&&(((##############(###############(###############(#####
51 | .%(#(#%@#((###(#((########&@@(((((#####((######(##(%((#######(#######(#####(#####((##(#######(#######(#######(#######(#######(#######(#####
52 | #@#(#((##((#####(#&(((#&(((@#######(##########(&%%#(#((##(#########(############((((###############(###############(###############(#####
53 | *%(((#(#(#(#(#(#%@#(((((((((##(((((((#(#(#(#(#(#(#@###(((#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#
54 | ,&(##(#(#####((%@#(###((((((((####((###(########((%@#(###############(###############(###############(###############(###############(#####
55 | ((((#(##(####&%(##(((((%@%((#(#######(######(##%@((((######(#######(#######(#######(#######(#######(#######(#######(#######(#######(#####
56 | %###((#(##########((###((##((##########(####(#((&##(((###############(###############(###############(###############(###############(#####
57 | (#(#(###(###(##((###(###((##(###(###(###((#&######(###(###(###(###(###(###(###(###(###(###(###(###(###(###(###(###(###(###(###(###(###(#
58 | %%(#(###(#(#(########(#####(#########(###((#&%#(#####(###############(###############(###############(###############(#############(((#####
59 | (&%%#%#%#((#(#####(#######(#######(######((#&%(###(#######(#######(#######(#######(#######(#######(#######(#######((##(##((#(#(###(((###
60 | ##(#(####(###############(######(((((####(###############(###############(###############(#############(##(#((##%%&@@@@&&&%%%%%
61 | &((((((((((((((((((((((((((((((((((((##((((((((((((((((((((((((((((((((((((((((((((((((((((((((##(((##&@@&&%%%%%%%%%%%%%%%%%%
62 | (((#(###############(#############%&((((############(###############(###############(#######%&@&%%%%%%%%%%%%%%%%%%%%%%%%%%
63 | @####(#######(#######(#######(##(%&(((#(#####(#######(#######(#######(#######(#######((##%@&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
64 | @#(((###############(#######((#@#(##(###############(###############(##############(%@&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
65 | &(#(###(###(###(###(###(##((###(((###(###(###(###(###(###(###(###(###(###(#####@&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
66 | /%((###############(#####((######(###############(###############(#(########&&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
67 | (#######(#######(######(%%##((##(#######(#######(#######(#######(####(((#@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
68 | @################(######(%%##((##((##############(###############(##(###@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
69 | #((#(#(#(#(#(#(#(#(#(#(@##(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(#(##@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
70 | (%(((###########(#######(#&#(#(###############(################(#@&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
71 | ##(####(#######(#######(#(#%((#######(#######(#######(#######(&@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
72 | /%(((###(#####(########(###(#&%###############(############((#@&%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%*/
73 |
74 | /// @title Pages NFT
75 | /// @author FrankieIsLost
76 | /// @author transmissions11
77 | /// @notice Pages is an ERC721 that can hold custom art.
78 | contract Pages is PagesERC721, LogisticToLinearVRGDA {
79 | using LibString for uint256;
80 |
81 | /*//////////////////////////////////////////////////////////////
82 | ADDRESSES
83 | //////////////////////////////////////////////////////////////*/
84 |
85 | /// @notice The address of the goo ERC20 token contract.
86 | Goo public immutable goo;
87 |
88 | /// @notice The address which receives pages reserved for the community.
89 | address public immutable community;
90 |
91 | /*//////////////////////////////////////////////////////////////
92 | URIS
93 | //////////////////////////////////////////////////////////////*/
94 |
95 | /// @notice Base URI for minted pages.
96 | string public BASE_URI;
97 |
98 | /*//////////////////////////////////////////////////////////////
99 | VRGDA INPUT STATE
100 | //////////////////////////////////////////////////////////////*/
101 |
102 | /// @notice Timestamp for the start of the VRGDA mint.
103 | uint256 public immutable mintStart;
104 |
105 | /// @notice Id of the most recently minted page.
106 | /// @dev Will be 0 if no pages have been minted yet.
107 | uint128 public currentId;
108 |
109 | /*//////////////////////////////////////////////////////////////
110 | COMMUNITY PAGES STATE
111 | //////////////////////////////////////////////////////////////*/
112 |
113 | /// @notice The number of pages minted to the community reserve.
114 | uint128 public numMintedForCommunity;
115 |
116 | /*//////////////////////////////////////////////////////////////
117 | PRICING CONSTANTS
118 | //////////////////////////////////////////////////////////////*/
119 |
120 | /// @dev The day the switch from a logistic to translated linear VRGDA is targeted to occur.
121 | /// @dev Represented as an 18 decimal fixed point number.
122 | int256 internal constant SWITCH_DAY_WAD = 233e18;
123 |
124 | /// @notice The minimum amount of pages that must be sold for the VRGDA issuance
125 | /// schedule to switch from logistic to the "post switch" translated linear formula.
126 | /// @dev Computed off-chain by plugging SWITCH_DAY_WAD into the uninverted pacing formula.
127 | /// @dev Represented as an 18 decimal fixed point number.
128 | int256 internal constant SOLD_BY_SWITCH_WAD = 8336.760939794622713006e18;
129 |
130 | /*//////////////////////////////////////////////////////////////
131 | EVENTS
132 | //////////////////////////////////////////////////////////////*/
133 |
134 | event PagePurchased(address indexed user, uint256 indexed pageId, uint256 price);
135 |
136 | event CommunityPagesMinted(address indexed user, uint256 lastMintedPageId, uint256 numPages);
137 |
138 | /*//////////////////////////////////////////////////////////////
139 | ERRORS
140 | //////////////////////////////////////////////////////////////*/
141 |
142 | error ReserveImbalance();
143 |
144 | error PriceExceededMax(uint256 currentPrice);
145 |
146 | /*//////////////////////////////////////////////////////////////
147 | CONSTRUCTOR
148 | //////////////////////////////////////////////////////////////*/
149 |
150 | /// @notice Sets VRGDA parameters, mint start, relevant addresses, and base URI.
151 | /// @param _mintStart Timestamp for the start of the VRGDA mint.
152 | /// @param _goo Address of the Goo contract.
153 | /// @param _community Address of the community reserve.
154 | /// @param _artGobblers Address of the ArtGobblers contract.
155 | /// @param _baseUri Base URI for token metadata.
156 | constructor(
157 | // Mint config:
158 | uint256 _mintStart,
159 | // Addresses:
160 | Goo _goo,
161 | address _community,
162 | ArtGobblers _artGobblers,
163 | // URIs:
164 | string memory _baseUri
165 | )
166 | PagesERC721(_artGobblers, "Pages", "PAGE")
167 | LogisticToLinearVRGDA(
168 | 4.2069e18, // Target price.
169 | 0.31e18, // Price decay percent.
170 | 9000e18, // Logistic asymptote.
171 | 0.014e18, // Logistic time scale.
172 | SOLD_BY_SWITCH_WAD, // Sold by switch.
173 | SWITCH_DAY_WAD, // Target switch day.
174 | 9e18 // Pages to target per day.
175 | )
176 | {
177 | mintStart = _mintStart;
178 |
179 | goo = _goo;
180 |
181 | community = _community;
182 |
183 | BASE_URI = _baseUri;
184 | }
185 |
186 | /*//////////////////////////////////////////////////////////////
187 | MINTING LOGIC
188 | //////////////////////////////////////////////////////////////*/
189 |
190 | /// @notice Mint a page with goo, burning the cost.
191 | /// @param maxPrice Maximum price to pay to mint the page.
192 | /// @param useVirtualBalance Whether the cost is paid from the
193 | /// user's virtual goo balance, or from their ERC20 goo balance.
194 | /// @return pageId The id of the page that was minted.
195 | function mintFromGoo(uint256 maxPrice, bool useVirtualBalance) external returns (uint256 pageId) {
196 | // Will revert if prior to mint start.
197 | uint256 currentPrice = pagePrice();
198 |
199 | // If the current price is above the user's specified max, revert.
200 | if (currentPrice > maxPrice) revert PriceExceededMax(currentPrice);
201 |
202 | // Decrement the user's goo balance by the current
203 | // price, either from virtual balance or ERC20 balance.
204 | useVirtualBalance
205 | ? artGobblers.burnGooForPages(msg.sender, currentPrice)
206 | : goo.burnForPages(msg.sender, currentPrice);
207 |
208 | unchecked {
209 | emit PagePurchased(msg.sender, pageId = ++currentId, currentPrice);
210 |
211 | _mint(msg.sender, pageId);
212 | }
213 | }
214 |
215 | /// @notice Calculate the mint cost of a page.
216 | /// @dev If the number of sales is below a pre-defined threshold, we use the
217 | /// VRGDA pricing algorithm, otherwise we use the post-switch pricing formula.
218 | /// @dev Reverts due to underflow if minting hasn't started yet. Done to save gas.
219 | function pagePrice() public view returns (uint256) {
220 | // We need checked math here to cause overflow
221 | // before minting has begun, preventing mints.
222 | uint256 timeSinceStart = block.timestamp - mintStart;
223 |
224 | unchecked {
225 | // The number of pages minted for the community reserve
226 | // should never exceed 10% of the total supply of pages.
227 | return getVRGDAPrice(toDaysWadUnsafe(timeSinceStart), currentId - numMintedForCommunity);
228 | }
229 | }
230 |
231 | /*//////////////////////////////////////////////////////////////
232 | COMMUNITY PAGES MINTING LOGIC
233 | //////////////////////////////////////////////////////////////*/
234 |
235 | /// @notice Mint a number of pages to the community reserve.
236 | /// @param numPages The number of pages to mint to the reserve.
237 | /// @dev Pages minted to the reserve cannot comprise more than 10% of the sum of the
238 | /// supply of goo minted pages and the supply of pages minted to the community reserve.
239 | function mintCommunityPages(uint256 numPages) external returns (uint256 lastMintedPageId) {
240 | unchecked {
241 | // Optimistically increment numMintedForCommunity, may be reverted below.
242 | // Overflow in this calculation is possible but numPages would have to be so
243 | // large that it would cause the loop in _batchMint to run out of gas quickly.
244 | uint256 newNumMintedForCommunity = numMintedForCommunity += uint128(numPages);
245 |
246 | // Ensure that after this mint pages minted to the community reserve won't comprise more than
247 | // 10% of the new total page supply. currentId is equivalent to the current total supply of pages.
248 | if (newNumMintedForCommunity > ((lastMintedPageId = currentId) + numPages) / 10) revert ReserveImbalance();
249 |
250 | // Mint the pages to the community reserve and update lastMintedPageId once minting is complete.
251 | lastMintedPageId = _batchMint(community, numPages, lastMintedPageId);
252 |
253 | currentId = uint128(lastMintedPageId); // Update currentId with the last minted page id.
254 |
255 | emit CommunityPagesMinted(msg.sender, lastMintedPageId, numPages);
256 | }
257 | }
258 |
259 | /*//////////////////////////////////////////////////////////////
260 | TOKEN URI LOGIC
261 | //////////////////////////////////////////////////////////////*/
262 |
263 | /// @notice Returns a page's URI if it has been minted.
264 | /// @param pageId The id of the page to get the URI for.
265 | function tokenURI(uint256 pageId) public view virtual override returns (string memory) {
266 | if (pageId == 0 || pageId > currentId) revert("NOT_MINTED");
267 |
268 | return string.concat(BASE_URI, pageId.toString());
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/src/utils/GobblerReserve.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {Owned} from "solmate/auth/Owned.sol";
5 |
6 | import {ArtGobblers} from "../ArtGobblers.sol";
7 |
8 | /// @title Gobbler Reserve
9 | /// @author FrankieIsLost
10 | /// @author transmissions11
11 | /// @notice Reserves gobblers for an owner while keeping any goo produced.
12 | contract GobblerReserve is Owned {
13 | /*//////////////////////////////////////////////////////////////
14 | ADDRESSES
15 | //////////////////////////////////////////////////////////////*/
16 |
17 | /// @notice Art Gobblers contract address.
18 | ArtGobblers public immutable artGobblers;
19 |
20 | /// @notice Sets the addresses of relevant contracts and users.
21 | /// @param _artGobblers The address of the ArtGobblers contract.
22 | /// @param _owner The address of the owner of Gobbler Reserve.
23 | constructor(ArtGobblers _artGobblers, address _owner) Owned(_owner) {
24 | artGobblers = _artGobblers;
25 | }
26 |
27 | /*//////////////////////////////////////////////////////////////
28 | WITHDRAWAL LOGIC
29 | //////////////////////////////////////////////////////////////*/
30 |
31 | /// @notice Withdraw gobblers from the reserve.
32 | /// @param to The address to transfer the gobblers to.
33 | /// @param ids The ids of the gobblers to transfer.
34 | function withdraw(address to, uint256[] calldata ids) external onlyOwner {
35 | // This is quite inefficient, but that's fine, it's not a hot path.
36 | unchecked {
37 | for (uint256 i = 0; i < ids.length; ++i) {
38 | artGobblers.transferFrom(address(this), to, ids[i]);
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/utils/rand/ChainlinkV1RandProvider.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {VRFConsumerBase} from "chainlink/v0.8/VRFConsumerBase.sol";
5 |
6 | import {ArtGobblers} from "../../ArtGobblers.sol";
7 |
8 | import {RandProvider} from "./RandProvider.sol";
9 |
10 | /// @title Chainlink V1 Randomness Provider.
11 | /// @author FrankieIsLost
12 | /// @author transmissions11
13 | /// @notice RandProvider wrapper around Chainlink VRF v1.
14 | contract ChainlinkV1RandProvider is RandProvider, VRFConsumerBase {
15 | /*//////////////////////////////////////////////////////////////
16 | ADDRESSES
17 | //////////////////////////////////////////////////////////////*/
18 |
19 | /// @notice The address of the Art Gobblers contract.
20 | ArtGobblers public immutable artGobblers;
21 |
22 | /*//////////////////////////////////////////////////////////////
23 | VRF CONFIGURATION
24 | //////////////////////////////////////////////////////////////*/
25 |
26 | /// @dev Public key to generate randomness against.
27 | bytes32 internal immutable chainlinkKeyHash;
28 |
29 | /// @dev Fee required to fulfill a VRF request.
30 | uint256 internal immutable chainlinkFee;
31 |
32 | /*//////////////////////////////////////////////////////////////
33 | ERRORS
34 | //////////////////////////////////////////////////////////////*/
35 |
36 | error NotGobblers();
37 |
38 | /*//////////////////////////////////////////////////////////////
39 | CONSTRUCTOR
40 | //////////////////////////////////////////////////////////////*/
41 |
42 | /// @notice Sets relevant addresses and VRF parameters.
43 | /// @param _artGobblers Address of the ArtGobblers contract.
44 | /// @param _vrfCoordinator Address of the VRF coordinator.
45 | /// @param _linkToken Address of the LINK token contract.
46 | /// @param _chainlinkKeyHash Public key to generate randomness against.
47 | /// @param _chainlinkFee Fee required to fulfill a VRF request.
48 | constructor(
49 | ArtGobblers _artGobblers,
50 | address _vrfCoordinator,
51 | address _linkToken,
52 | bytes32 _chainlinkKeyHash,
53 | uint256 _chainlinkFee
54 | ) VRFConsumerBase(_vrfCoordinator, _linkToken) {
55 | artGobblers = _artGobblers;
56 |
57 | chainlinkKeyHash = _chainlinkKeyHash;
58 | chainlinkFee = _chainlinkFee;
59 | }
60 |
61 | /// @notice Request random bytes from Chainlink VRF. Can only by called by the ArtGobblers contract.
62 | function requestRandomBytes() external returns (bytes32 requestId) {
63 | // The caller must be the ArtGobblers contract, revert otherwise.
64 | if (msg.sender != address(artGobblers)) revert NotGobblers();
65 |
66 | // The requestRandomness call will revert if we don't have enough LINK to afford the request.
67 | emit RandomBytesRequested(requestId = requestRandomness(chainlinkKeyHash, chainlinkFee));
68 | }
69 |
70 | /// @dev Handles VRF response by calling back into the ArtGobblers contract.
71 | function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override {
72 | emit RandomBytesReturned(requestId, randomness);
73 |
74 | artGobblers.acceptRandomSeed(requestId, randomness);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/utils/rand/RandProvider.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | /// @title Randomness Provider Interface.
5 | /// @author FrankieIsLost
6 | /// @author transmissions11
7 | /// @notice Generic asynchronous randomness provider interface.
8 | interface RandProvider {
9 | /*//////////////////////////////////////////////////////////////
10 | EVENTS
11 | //////////////////////////////////////////////////////////////*/
12 |
13 | event RandomBytesRequested(bytes32 requestId);
14 | event RandomBytesReturned(bytes32 requestId, uint256 randomness);
15 |
16 | /*//////////////////////////////////////////////////////////////
17 | FUNCTIONS
18 | //////////////////////////////////////////////////////////////*/
19 |
20 | /// @dev Request random bytes from the randomness provider.
21 | function requestRandomBytes() external returns (bytes32 requestId);
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils/token/GobblersERC721.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: AGPL-3.0-only
2 | pragma solidity >=0.8.0;
3 |
4 | import {ERC721TokenReceiver} from "solmate/tokens/ERC721.sol";
5 |
6 | /// @notice ERC721 implementation optimized for ArtGobblers by packing balanceOf/ownerOf with user/attribute data.
7 | /// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol)
8 | abstract contract GobblersERC721 {
9 | /*//////////////////////////////////////////////////////////////
10 | EVENTS
11 | //////////////////////////////////////////////////////////////*/
12 |
13 | event Transfer(address indexed from, address indexed to, uint256 indexed id);
14 |
15 | event Approval(address indexed owner, address indexed spender, uint256 indexed id);
16 |
17 | event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
18 |
19 | /*//////////////////////////////////////////////////////////////
20 | METADATA STORAGE/LOGIC
21 | //////////////////////////////////////////////////////////////*/
22 |
23 | string public name;
24 |
25 | string public symbol;
26 |
27 | function tokenURI(uint256 id) external view virtual returns (string memory);
28 |
29 | /*//////////////////////////////////////////////////////////////
30 | GOBBLERS/ERC721 STORAGE
31 | //////////////////////////////////////////////////////////////*/
32 |
33 | /// @notice Struct holding gobbler data.
34 | struct GobblerData {
35 | // The current owner of the gobbler.
36 | address owner;
37 | // Index of token after shuffle.
38 | uint64 idx;
39 | // Multiple on goo issuance.
40 | uint32 emissionMultiple;
41 | }
42 |
43 | /// @notice Maps gobbler ids to their data.
44 | mapping(uint256 => GobblerData) public getGobblerData;
45 |
46 | /// @notice Struct holding data relevant to each user's account.
47 | struct UserData {
48 | // The total number of gobblers currently owned by the user.
49 | uint32 gobblersOwned;
50 | // The sum of the multiples of all gobblers the user holds.
51 | uint32 emissionMultiple;
52 | // User's goo balance at time of last checkpointing.
53 | uint128 lastBalance;
54 | // Timestamp of the last goo balance checkpoint.
55 | uint64 lastTimestamp;
56 | }
57 |
58 | /// @notice Maps user addresses to their account data.
59 | mapping(address => UserData) public getUserData;
60 |
61 | function ownerOf(uint256 id) external view returns (address owner) {
62 | require((owner = getGobblerData[id].owner) != address(0), "NOT_MINTED");
63 | }
64 |
65 | function balanceOf(address owner) external view returns (uint256) {
66 | require(owner != address(0), "ZERO_ADDRESS");
67 |
68 | return getUserData[owner].gobblersOwned;
69 | }
70 |
71 | /*//////////////////////////////////////////////////////////////
72 | ERC721 APPROVAL STORAGE
73 | //////////////////////////////////////////////////////////////*/
74 |
75 | mapping(uint256 => address) public getApproved;
76 |
77 | mapping(address => mapping(address => bool)) public isApprovedForAll;
78 |
79 | /*//////////////////////////////////////////////////////////////
80 | CONSTRUCTOR
81 | //////////////////////////////////////////////////////////////*/
82 |
83 | constructor(string memory _name, string memory _symbol) {
84 | name = _name;
85 | symbol = _symbol;
86 | }
87 |
88 | /*//////////////////////////////////////////////////////////////
89 | ERC721 LOGIC
90 | //////////////////////////////////////////////////////////////*/
91 |
92 | function approve(address spender, uint256 id) external {
93 | address owner = getGobblerData[id].owner;
94 |
95 | require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED");
96 |
97 | getApproved[id] = spender;
98 |
99 | emit Approval(owner, spender, id);
100 | }
101 |
102 | function setApprovalForAll(address operator, bool approved) external {
103 | isApprovedForAll[msg.sender][operator] = approved;
104 |
105 | emit ApprovalForAll(msg.sender, operator, approved);
106 | }
107 |
108 | function transferFrom(
109 | address from,
110 | address to,
111 | uint256 id
112 | ) public virtual;
113 |
114 | function safeTransferFrom(
115 | address from,
116 | address to,
117 | uint256 id
118 | ) external {
119 | transferFrom(from, to, id);
120 |
121 | require(
122 | to.code.length == 0 ||
123 | ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") ==
124 | ERC721TokenReceiver.onERC721Received.selector,
125 | "UNSAFE_RECIPIENT"
126 | );
127 | }
128 |
129 | function safeTransferFrom(
130 | address from,
131 | address to,
132 | uint256 id,
133 | bytes calldata data
134 | ) external {
135 | transferFrom(from, to, id);
136 |
137 | require(
138 | to.code.length == 0 ||
139 | ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) ==
140 | ERC721TokenReceiver.onERC721Received.selector,
141 | "UNSAFE_RECIPIENT"
142 | );
143 | }
144 |
145 | /*//////////////////////////////////////////////////////////////
146 | ERC165 LOGIC
147 | //////////////////////////////////////////////////////////////*/
148 |
149 | function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
150 | return
151 | interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
152 | interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
153 | interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata
154 | }
155 |
156 | /*//////////////////////////////////////////////////////////////
157 | INTERNAL MINT LOGIC
158 | //////////////////////////////////////////////////////////////*/
159 |
160 | function _mint(address to, uint256 id) internal {
161 | // Does not check if the token was already minted or the recipient is address(0)
162 | // because ArtGobblers.sol manages its ids in such a way that it ensures it won't
163 | // double mint and will only mint to safe addresses or msg.sender who cannot be zero.
164 |
165 | unchecked {
166 | ++getUserData[to].gobblersOwned;
167 | }
168 |
169 | getGobblerData[id].owner = to;
170 |
171 | emit Transfer(address(0), to, id);
172 | }
173 |
174 | function _batchMint(
175 | address to,
176 | uint256 amount,
177 | uint256 lastMintedId
178 | ) internal returns (uint256) {
179 | // Doesn't check if the tokens were already minted or the recipient is address(0)
180 | // because ArtGobblers.sol manages its ids in such a way that it ensures it won't
181 | // double mint and will only mint to safe addresses or msg.sender who cannot be zero.
182 |
183 | unchecked {
184 | getUserData[to].gobblersOwned += uint32(amount);
185 |
186 | for (uint256 i = 0; i < amount; ++i) {
187 | getGobblerData[++lastMintedId].owner = to;
188 |
189 | emit Transfer(address(0), to, lastMintedId);
190 | }
191 | }
192 |
193 | return lastMintedId;
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/src/utils/token/PagesERC721.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {ERC721TokenReceiver} from "solmate/tokens/ERC721.sol";
5 | import {ArtGobblers} from "../../ArtGobblers.sol";
6 |
7 | /// @notice ERC721 implementation optimized for Pages by pre-approving them to the ArtGobblers contract.
8 | /// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol)
9 | abstract contract PagesERC721 {
10 | /*//////////////////////////////////////////////////////////////
11 | EVENTS
12 | //////////////////////////////////////////////////////////////*/
13 |
14 | event Transfer(address indexed from, address indexed to, uint256 indexed id);
15 |
16 | event Approval(address indexed owner, address indexed spender, uint256 indexed id);
17 |
18 | event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
19 |
20 | /*//////////////////////////////////////////////////////////////
21 | METADATA STORAGE/LOGIC
22 | //////////////////////////////////////////////////////////////*/
23 |
24 | string public name;
25 |
26 | string public symbol;
27 |
28 | function tokenURI(uint256 id) external view virtual returns (string memory);
29 |
30 | /*//////////////////////////////////////////////////////////////
31 | CONSTRUCTOR
32 | //////////////////////////////////////////////////////////////*/
33 |
34 | ArtGobblers public immutable artGobblers;
35 |
36 | constructor(
37 | ArtGobblers _artGobblers,
38 | string memory _name,
39 | string memory _symbol
40 | ) {
41 | name = _name;
42 | symbol = _symbol;
43 | artGobblers = _artGobblers;
44 | }
45 |
46 | /*//////////////////////////////////////////////////////////////
47 | ERC721 BALANCE/OWNER STORAGE
48 | //////////////////////////////////////////////////////////////*/
49 |
50 | mapping(uint256 => address) internal _ownerOf;
51 |
52 | mapping(address => uint256) internal _balanceOf;
53 |
54 | function ownerOf(uint256 id) external view returns (address owner) {
55 | require((owner = _ownerOf[id]) != address(0), "NOT_MINTED");
56 | }
57 |
58 | function balanceOf(address owner) external view returns (uint256) {
59 | require(owner != address(0), "ZERO_ADDRESS");
60 |
61 | return _balanceOf[owner];
62 | }
63 |
64 | /*//////////////////////////////////////////////////////////////
65 | ERC721 APPROVAL STORAGE
66 | //////////////////////////////////////////////////////////////*/
67 |
68 | mapping(uint256 => address) public getApproved;
69 |
70 | mapping(address => mapping(address => bool)) internal _isApprovedForAll;
71 |
72 | function isApprovedForAll(address owner, address operator) public view returns (bool isApproved) {
73 | if (operator == address(artGobblers)) return true; // Skip approvals for the ArtGobblers contract.
74 |
75 | return _isApprovedForAll[owner][operator];
76 | }
77 |
78 | /*//////////////////////////////////////////////////////////////
79 | ERC721 LOGIC
80 | //////////////////////////////////////////////////////////////*/
81 |
82 | function approve(address spender, uint256 id) external {
83 | address owner = _ownerOf[id];
84 |
85 | require(msg.sender == owner || isApprovedForAll(owner, msg.sender), "NOT_AUTHORIZED");
86 |
87 | getApproved[id] = spender;
88 |
89 | emit Approval(owner, spender, id);
90 | }
91 |
92 | function setApprovalForAll(address operator, bool approved) external {
93 | _isApprovedForAll[msg.sender][operator] = approved;
94 |
95 | emit ApprovalForAll(msg.sender, operator, approved);
96 | }
97 |
98 | function transferFrom(
99 | address from,
100 | address to,
101 | uint256 id
102 | ) public {
103 | require(from == _ownerOf[id], "WRONG_FROM");
104 |
105 | require(to != address(0), "INVALID_RECIPIENT");
106 |
107 | require(
108 | msg.sender == from || isApprovedForAll(from, msg.sender) || msg.sender == getApproved[id],
109 | "NOT_AUTHORIZED"
110 | );
111 |
112 | // Underflow of the sender's balance is impossible because we check for
113 | // ownership above and the recipient's balance can't realistically overflow.
114 | unchecked {
115 | _balanceOf[from]--;
116 |
117 | _balanceOf[to]++;
118 | }
119 |
120 | _ownerOf[id] = to;
121 |
122 | delete getApproved[id];
123 |
124 | emit Transfer(from, to, id);
125 | }
126 |
127 | function safeTransferFrom(
128 | address from,
129 | address to,
130 | uint256 id
131 | ) external {
132 | transferFrom(from, to, id);
133 |
134 | if (to.code.length != 0)
135 | require(
136 | ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") ==
137 | ERC721TokenReceiver.onERC721Received.selector,
138 | "UNSAFE_RECIPIENT"
139 | );
140 | }
141 |
142 | function safeTransferFrom(
143 | address from,
144 | address to,
145 | uint256 id,
146 | bytes calldata data
147 | ) external {
148 | transferFrom(from, to, id);
149 |
150 | if (to.code.length != 0)
151 | require(
152 | ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) ==
153 | ERC721TokenReceiver.onERC721Received.selector,
154 | "UNSAFE_RECIPIENT"
155 | );
156 | }
157 |
158 | /*//////////////////////////////////////////////////////////////
159 | ERC165 LOGIC
160 | //////////////////////////////////////////////////////////////*/
161 |
162 | function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
163 | return
164 | interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165
165 | interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721
166 | interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata
167 | }
168 |
169 | /*//////////////////////////////////////////////////////////////
170 | INTERNAL MINT LOGIC
171 | //////////////////////////////////////////////////////////////*/
172 |
173 | function _mint(address to, uint256 id) internal {
174 | // Does not check the token has not been already minted
175 | // or is being minted to address(0) because ids in Pages.sol
176 | // are set using a monotonically increasing counter and only
177 | // minted to safe addresses or msg.sender who cannot be zero.
178 |
179 | // Counter overflow is incredibly unrealistic.
180 | unchecked {
181 | _balanceOf[to]++;
182 | }
183 |
184 | _ownerOf[id] = to;
185 |
186 | emit Transfer(address(0), to, id);
187 | }
188 |
189 | function _batchMint(
190 | address to,
191 | uint256 amount,
192 | uint256 lastMintedId
193 | ) internal returns (uint256) {
194 | // Doesn't check if the tokens were already minted or the recipient is address(0)
195 | // because Pages.sol manages its ids in a way that it ensures it won't double
196 | // mint and will only mint to safe addresses or msg.sender who cannot be zero.
197 |
198 | unchecked {
199 | _balanceOf[to] += amount;
200 |
201 | for (uint256 i = 0; i < amount; ++i) {
202 | _ownerOf[++lastMintedId] = to;
203 |
204 | emit Transfer(address(0), to, lastMintedId);
205 | }
206 | }
207 |
208 | return lastMintedId;
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/test/Benchmarks.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DSTest} from "ds-test/test.sol";
5 | import {Utilities} from "./utils/Utilities.sol";
6 | import {console} from "./utils/Console.sol";
7 | import {Vm} from "forge-std/Vm.sol";
8 | import {ArtGobblers} from "../src/ArtGobblers.sol";
9 | import {RandProvider} from "../src/utils/rand/RandProvider.sol";
10 | import {ChainlinkV1RandProvider} from "../src/utils/rand/ChainlinkV1RandProvider.sol";
11 | import {Goo} from "../src/Goo.sol";
12 | import {Pages} from "../src/Pages.sol";
13 | import {LinkToken} from "./utils/mocks/LinkToken.sol";
14 | import {VRFCoordinatorMock} from "chainlink/v0.8/mocks/VRFCoordinatorMock.sol";
15 |
16 | contract BenchmarksTest is DSTest {
17 | Vm internal immutable vm = Vm(HEVM_ADDRESS);
18 |
19 | Utilities internal utils;
20 | address payable[] internal users;
21 |
22 | ArtGobblers private gobblers;
23 | VRFCoordinatorMock private vrfCoordinator;
24 | LinkToken private linkToken;
25 | RandProvider private randProvider;
26 | Goo private goo;
27 | Pages private pages;
28 |
29 | address gobblerAddress;
30 | address pageAddress;
31 |
32 | uint256 legendaryCost;
33 |
34 | bytes32 private keyHash;
35 | uint256 private fee;
36 |
37 | function setUp() public {
38 | vm.warp(1); // Otherwise mintStart will be set to 0 and brick pages.mintFromGoo(type(uint256).max)
39 |
40 | utils = new Utilities();
41 | users = utils.createUsers(5);
42 | linkToken = new LinkToken();
43 | vrfCoordinator = new VRFCoordinatorMock(address(linkToken));
44 |
45 | // Gobblers contract will be deployed after 2 contract deploys, and pages after 3.
46 | gobblerAddress = utils.predictContractAddress(address(this), 2);
47 | pageAddress = utils.predictContractAddress(address(this), 3);
48 |
49 | randProvider = new ChainlinkV1RandProvider(
50 | ArtGobblers(gobblerAddress),
51 | address(vrfCoordinator),
52 | address(linkToken),
53 | keyHash,
54 | fee
55 | );
56 |
57 | goo = new Goo(gobblerAddress, pageAddress);
58 |
59 | gobblers = new ArtGobblers(
60 | keccak256(abi.encodePacked(users[0])),
61 | block.timestamp,
62 | goo,
63 | Pages(pageAddress),
64 | address(0xBEEF),
65 | address(0xBEEF),
66 | randProvider,
67 | "base",
68 | "",
69 | keccak256(abi.encodePacked("provenance"))
70 | );
71 |
72 | pages = new Pages(block.timestamp, goo, address(0xBEEF), gobblers, "");
73 |
74 | vm.prank(address(gobblers));
75 | goo.mintForGobblers(address(this), type(uint192).max);
76 |
77 | gobblers.addGoo(type(uint96).max);
78 |
79 | mintPageToAddress(address(this), 9);
80 | mintGobblerToAddress(address(this), gobblers.LEGENDARY_AUCTION_INTERVAL());
81 |
82 | vm.warp(block.timestamp + 30 days);
83 |
84 | legendaryCost = gobblers.legendaryGobblerPrice();
85 |
86 | bytes32 requestId = gobblers.requestRandomSeed();
87 | uint256 randomness = uint256(keccak256(abi.encodePacked("seed")));
88 | vrfCoordinator.callBackWithRandomness(requestId, randomness, address(randProvider));
89 | }
90 |
91 | function testPagePrice() public view {
92 | pages.pagePrice();
93 | }
94 |
95 | function testGobblerPrice() public view {
96 | gobblers.gobblerPrice();
97 | }
98 |
99 | function testLegendaryGobblersPrice() public view {
100 | gobblers.legendaryGobblerPrice();
101 | }
102 |
103 | function testGooBalance() public view {
104 | gobblers.gooBalance(address(this));
105 | }
106 |
107 | function testMintPage() public {
108 | pages.mintFromGoo(type(uint256).max, false);
109 | }
110 |
111 | function testMintPageUsingVirtualBalance() public {
112 | pages.mintFromGoo(type(uint256).max, true);
113 | }
114 |
115 | function testMintGobbler() public {
116 | gobblers.mintFromGoo(type(uint256).max, false);
117 | }
118 |
119 | function testMintGobblerUsingVirtualBalance() public {
120 | gobblers.mintFromGoo(type(uint256).max, true);
121 | }
122 |
123 | function testTransferGobbler() public {
124 | gobblers.transferFrom(address(this), address(0xBEEF), 1);
125 | }
126 |
127 | function testAddGoo() public {
128 | gobblers.addGoo(1e18);
129 | }
130 |
131 | function testRemoveGoo() public {
132 | gobblers.removeGoo(1e18);
133 | }
134 |
135 | function testRevealGobblers() public {
136 | gobblers.revealGobblers(100);
137 | }
138 |
139 | function testMintLegendaryGobbler() public {
140 | uint256 legendaryGobblerCost = legendaryCost;
141 |
142 | uint256[] memory ids = new uint256[](legendaryGobblerCost);
143 | for (uint256 i = 0; i < legendaryGobblerCost; ++i) ids[i] = i + 1;
144 |
145 | gobblers.mintLegendaryGobbler(ids);
146 | }
147 |
148 | function testMintReservedGobblers() public {
149 | gobblers.mintReservedGobblers(1);
150 | }
151 |
152 | function testMintCommunityPages() public {
153 | pages.mintCommunityPages(1);
154 | }
155 |
156 | function testDeployGobblers() public {
157 | new ArtGobblers(
158 | keccak256(abi.encodePacked(users[0])),
159 | block.timestamp,
160 | goo,
161 | Pages(pageAddress),
162 | address(0xBEEF),
163 | address(0xBEEF),
164 | randProvider,
165 | "base",
166 | "",
167 | keccak256(abi.encodePacked("provenance"))
168 | );
169 | }
170 |
171 | function testDeployGoo() public {
172 | new Goo(gobblerAddress, pageAddress);
173 | }
174 |
175 | function testDeployPages() public {
176 | new Pages(block.timestamp, goo, address(0xBEEF), gobblers, "");
177 | }
178 |
179 | function mintGobblerToAddress(address addr, uint256 num) internal {
180 | for (uint256 i = 0; i < num; ++i) {
181 | vm.startPrank(address(gobblers));
182 | goo.mintForGobblers(addr, gobblers.gobblerPrice());
183 | vm.stopPrank();
184 |
185 | vm.prank(addr);
186 | gobblers.mintFromGoo(type(uint256).max, false);
187 | }
188 | }
189 |
190 | function mintPageToAddress(address addr, uint256 num) internal {
191 | for (uint256 i = 0; i < num; ++i) {
192 | vm.startPrank(address(gobblers));
193 | goo.mintForGobblers(addr, pages.pagePrice());
194 | vm.stopPrank();
195 |
196 | vm.prank(addr);
197 | pages.mintFromGoo(type(uint256).max, false);
198 | }
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/test/GobblerReserve.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
5 | import {Utilities} from "./utils/Utilities.sol";
6 | import {console} from "./utils/Console.sol";
7 | import {Vm} from "forge-std/Vm.sol";
8 | import {stdError} from "forge-std/Test.sol";
9 | import {ArtGobblers} from "../src/ArtGobblers.sol";
10 | import {Goo} from "../src/Goo.sol";
11 | import {Pages} from "../src/Pages.sol";
12 | import {GobblerReserve} from "../src/utils/GobblerReserve.sol";
13 | import {RandProvider} from "../src/utils/rand/RandProvider.sol";
14 | import {ChainlinkV1RandProvider} from "../src/utils/rand/ChainlinkV1RandProvider.sol";
15 | import {LinkToken} from "./utils/mocks/LinkToken.sol";
16 | import {VRFCoordinatorMock} from "chainlink/v0.8/mocks/VRFCoordinatorMock.sol";
17 | import {ERC721} from "solmate/tokens/ERC721.sol";
18 | import {LibString} from "solmate/utils/LibString.sol";
19 |
20 | /// @notice Unit test for the Gobbler Reserve contract.
21 | contract GobblerReserveTest is DSTestPlus {
22 | using LibString for uint256;
23 |
24 | Vm internal immutable vm = Vm(HEVM_ADDRESS);
25 |
26 | Utilities internal utils;
27 | address payable[] internal users;
28 |
29 | ArtGobblers internal gobblers;
30 | VRFCoordinatorMock internal vrfCoordinator;
31 | LinkToken internal linkToken;
32 | Goo internal goo;
33 | Pages internal pages;
34 | GobblerReserve internal team;
35 | GobblerReserve internal community;
36 | RandProvider internal randProvider;
37 |
38 | bytes32 private keyHash;
39 | uint256 private fee;
40 |
41 | uint256[] ids;
42 |
43 | /*//////////////////////////////////////////////////////////////
44 | SETUP
45 | //////////////////////////////////////////////////////////////*/
46 |
47 | function setUp() public {
48 | utils = new Utilities();
49 | users = utils.createUsers(5);
50 | linkToken = new LinkToken();
51 | vrfCoordinator = new VRFCoordinatorMock(address(linkToken));
52 |
53 | // Gobblers contract will be deployed after 4 contract deploys, and pages after 5.
54 | address gobblerAddress = utils.predictContractAddress(address(this), 4);
55 | address pagesAddress = utils.predictContractAddress(address(this), 5);
56 |
57 | team = new GobblerReserve(ArtGobblers(gobblerAddress), address(this));
58 | community = new GobblerReserve(ArtGobblers(gobblerAddress), address(this));
59 | randProvider = new ChainlinkV1RandProvider(
60 | ArtGobblers(gobblerAddress),
61 | address(vrfCoordinator),
62 | address(linkToken),
63 | keyHash,
64 | fee
65 | );
66 |
67 | goo = new Goo(
68 | // Gobblers:
69 | utils.predictContractAddress(address(this), 1),
70 | // Pages:
71 | utils.predictContractAddress(address(this), 2)
72 | );
73 |
74 | gobblers = new ArtGobblers(
75 | keccak256(abi.encodePacked(users[0])),
76 | block.timestamp,
77 | goo,
78 | Pages(pagesAddress),
79 | address(team),
80 | address(community),
81 | randProvider,
82 | "base",
83 | "",
84 | keccak256(abi.encodePacked("provenance"))
85 | );
86 |
87 | pages = new Pages(block.timestamp, goo, address(0xBEEF), gobblers, "");
88 | }
89 |
90 | /*//////////////////////////////////////////////////////////////
91 | WITHDRAWAL TESTS
92 | //////////////////////////////////////////////////////////////*/
93 |
94 | /// @notice Tests that a reserve can be withdrawn from.
95 | function testCanWithdraw() public {
96 | mintGobblerToAddress(users[0], 9);
97 |
98 | gobblers.mintReservedGobblers(1);
99 |
100 | assertEq(gobblers.ownerOf(10), address(team));
101 | assertEq(gobblers.ownerOf(11), address(community));
102 |
103 | uint256[] memory idsToWithdraw = new uint256[](1);
104 |
105 | idsToWithdraw[0] = 10;
106 | team.withdraw(address(this), idsToWithdraw);
107 |
108 | idsToWithdraw[0] = 11;
109 | community.withdraw(address(this), idsToWithdraw);
110 |
111 | assertEq(gobblers.ownerOf(10), address(this));
112 | assertEq(gobblers.ownerOf(11), address(this));
113 | }
114 |
115 | /*//////////////////////////////////////////////////////////////
116 | HELPERS
117 | //////////////////////////////////////////////////////////////*/
118 |
119 | /// @notice Mint a number of gobblers to the given address
120 | function mintGobblerToAddress(address addr, uint256 num) internal {
121 | for (uint256 i = 0; i < num; ++i) {
122 | vm.startPrank(address(gobblers));
123 | goo.mintForGobblers(addr, gobblers.gobblerPrice());
124 | vm.stopPrank();
125 |
126 | vm.prank(addr);
127 | gobblers.mintFromGoo(type(uint256).max, false);
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/test/Goo.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DSTest} from "ds-test/test.sol";
5 | import {Utilities} from "./utils/Utilities.sol";
6 | import {Vm} from "forge-std/Vm.sol";
7 | import {stdError} from "forge-std/Test.sol";
8 | import {Goo} from "../src/Goo.sol";
9 |
10 | contract GooTest is DSTest {
11 | Vm internal immutable vm = Vm(HEVM_ADDRESS);
12 | Utilities internal utils;
13 | address payable[] internal users;
14 | Goo internal goo;
15 |
16 | function setUp() public {
17 | utils = new Utilities();
18 | users = utils.createUsers(5);
19 | goo = new Goo(address(this), users[0]);
20 | }
21 |
22 | function testMintByAuthority() public {
23 | uint256 initialSupply = goo.totalSupply();
24 | uint256 mintAmount = 100000;
25 | goo.mintForGobblers(address(this), mintAmount);
26 | uint256 finalSupply = goo.totalSupply();
27 | assertEq(finalSupply, initialSupply + mintAmount);
28 | }
29 |
30 | function testMintByNonAuthority() public {
31 | uint256 mintAmount = 100000;
32 | vm.prank(users[0]);
33 | vm.expectRevert(Goo.Unauthorized.selector);
34 | goo.mintForGobblers(address(this), mintAmount);
35 | }
36 |
37 | function testSetPages() public {
38 | goo.mintForGobblers(address(this), 1000000);
39 | uint256 initialSupply = goo.totalSupply();
40 | uint256 burnAmount = 100000;
41 | vm.prank(users[0]);
42 | goo.burnForPages(address(this), burnAmount);
43 | uint256 finalSupply = goo.totalSupply();
44 | assertEq(finalSupply, initialSupply - burnAmount);
45 | }
46 |
47 | function testBurnAllowed() public {
48 | uint256 mintAmount = 100000;
49 | goo.mintForGobblers(address(this), mintAmount);
50 | uint256 burnAmount = 30000;
51 | goo.burnForGobblers(address(this), burnAmount);
52 | uint256 finalBalance = goo.balanceOf(address(this));
53 | assertEq(finalBalance, mintAmount - burnAmount);
54 | }
55 |
56 | function testBurnNotAllowed() public {
57 | uint256 mintAmount = 100000;
58 | goo.mintForGobblers(address(this), mintAmount);
59 | uint256 burnAmount = 200000;
60 | vm.expectRevert(stdError.arithmeticError);
61 | goo.burnForGobblers(address(this), burnAmount);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/test/Optimizations.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
5 |
6 | contract OptimizationsTest is DSTestPlus {
7 | function testFuzzCurrentIdMultipleBranchlessOptimization(uint256 swapIndex) public {
8 | /*//////////////////////////////////////////////////////////////
9 | BRANCHLESS
10 | //////////////////////////////////////////////////////////////*/
11 |
12 | uint256 newCurrentIdMultipleBranchless = 9; // For beyond 7963.
13 | assembly {
14 | // prettier-ignore
15 | newCurrentIdMultipleBranchless := sub(sub(sub(
16 | newCurrentIdMultipleBranchless,
17 | lt(swapIndex, 7964)),
18 | lt(swapIndex, 5673)),
19 | lt(swapIndex, 3055)
20 | )
21 | }
22 |
23 | /*//////////////////////////////////////////////////////////////
24 | BRANCHED
25 | //////////////////////////////////////////////////////////////*/
26 |
27 | uint256 newCurrentIdMultipleBranched = 9; // For beyond 7963.
28 | if (swapIndex <= 3054) newCurrentIdMultipleBranched = 6;
29 | else if (swapIndex <= 5672) newCurrentIdMultipleBranched = 7;
30 | else if (swapIndex <= 7963) newCurrentIdMultipleBranched = 8;
31 |
32 | /*//////////////////////////////////////////////////////////////
33 | EQUIVALENCE
34 | //////////////////////////////////////////////////////////////*/
35 |
36 | assertEq(newCurrentIdMultipleBranchless, newCurrentIdMultipleBranched);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/test/Pages.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
5 | import {Utilities} from "./utils/Utilities.sol";
6 | import {Vm} from "forge-std/Vm.sol";
7 | import {stdError} from "forge-std/Test.sol";
8 | import {Goo} from "../src/Goo.sol";
9 | import {Pages} from "../src/Pages.sol";
10 | import {ArtGobblers} from "../src/ArtGobblers.sol";
11 | import {console} from "./utils/Console.sol";
12 | import {fromDaysWadUnsafe} from "solmate/utils/SignedWadMath.sol";
13 |
14 | contract PagesTest is DSTestPlus {
15 | Vm internal immutable vm = Vm(HEVM_ADDRESS);
16 | Utilities internal utils;
17 | address payable[] internal users;
18 | address internal mintAuth;
19 |
20 | address internal user;
21 | Goo internal goo;
22 | Pages internal pages;
23 | uint256 mintStart;
24 |
25 | address internal community = address(0xBEEF);
26 |
27 | function setUp() public {
28 | // Avoid starting at timestamp at 0 for ease of testing.
29 | vm.warp(block.timestamp + 1);
30 |
31 | utils = new Utilities();
32 | users = utils.createUsers(5);
33 |
34 | goo = new Goo(
35 | // Gobblers:
36 | address(this),
37 | // Pages:
38 | utils.predictContractAddress(address(this), 1)
39 | );
40 |
41 | pages = new Pages(block.timestamp, goo, community, ArtGobblers(address(this)), "");
42 |
43 | user = users[1];
44 | }
45 |
46 | function testMintBeforeSetMint() public {
47 | vm.expectRevert(stdError.arithmeticError);
48 | vm.prank(user);
49 | pages.mintFromGoo(type(uint256).max, false);
50 | }
51 |
52 | function testMintBeforeStart() public {
53 | vm.warp(block.timestamp - 1);
54 |
55 | vm.expectRevert(stdError.arithmeticError);
56 | vm.prank(user);
57 | pages.mintFromGoo(type(uint256).max, false);
58 | }
59 |
60 | function testRegularMint() public {
61 | goo.mintForGobblers(user, pages.pagePrice());
62 | vm.prank(user);
63 | pages.mintFromGoo(type(uint256).max, false);
64 | assertEq(user, pages.ownerOf(1));
65 | }
66 |
67 | function testTargetPrice() public {
68 | // Warp to the target sale time so that the page price equals the target price.
69 | vm.warp(block.timestamp + fromDaysWadUnsafe(pages.getTargetSaleTime(1e18)));
70 |
71 | uint256 cost = pages.pagePrice();
72 | assertRelApproxEq(cost, uint256(pages.targetPrice()), 0.00001e18);
73 | }
74 |
75 | function testMintCommunityPagesFailsWithNoMints() public {
76 | vm.expectRevert(Pages.ReserveImbalance.selector);
77 | pages.mintCommunityPages(1);
78 | }
79 |
80 | function testCanMintCommunity() public {
81 | mintPageToAddress(user, 9);
82 |
83 | pages.mintCommunityPages(1);
84 | assertEq(pages.ownerOf(10), address(community));
85 | }
86 |
87 | function testCanMintMultipleCommunity() public {
88 | mintPageToAddress(user, 90);
89 |
90 | pages.mintCommunityPages(10);
91 | assertEq(pages.ownerOf(91), address(community));
92 | assertEq(pages.ownerOf(92), address(community));
93 | assertEq(pages.ownerOf(93), address(community));
94 | assertEq(pages.ownerOf(94), address(community));
95 | assertEq(pages.ownerOf(95), address(community));
96 | assertEq(pages.ownerOf(96), address(community));
97 | assertEq(pages.ownerOf(97), address(community));
98 | assertEq(pages.ownerOf(98), address(community));
99 | assertEq(pages.ownerOf(99), address(community));
100 | assertEq(pages.ownerOf(100), address(community));
101 |
102 | assertEq(pages.numMintedForCommunity(), 10);
103 | assertEq(pages.currentId(), 100);
104 |
105 | // Ensure id doesn't get messed up.
106 | mintPageToAddress(user, 1);
107 | assertEq(pages.ownerOf(101), user);
108 | assertEq(pages.currentId(), 101);
109 | }
110 |
111 | function testCantMintTooFastCommunity() public {
112 | mintPageToAddress(user, 18);
113 |
114 | vm.expectRevert(Pages.ReserveImbalance.selector);
115 | pages.mintCommunityPages(3);
116 | }
117 |
118 | function testCantMintTooFastCommunityOneByOne() public {
119 | mintPageToAddress(user, 90);
120 |
121 | pages.mintCommunityPages(1);
122 | pages.mintCommunityPages(1);
123 | pages.mintCommunityPages(1);
124 | pages.mintCommunityPages(1);
125 | pages.mintCommunityPages(1);
126 | pages.mintCommunityPages(1);
127 | pages.mintCommunityPages(1);
128 | pages.mintCommunityPages(1);
129 | pages.mintCommunityPages(1);
130 | pages.mintCommunityPages(1);
131 |
132 | vm.expectRevert(Pages.ReserveImbalance.selector);
133 | pages.mintCommunityPages(1);
134 | }
135 |
136 | /// @notice Test that the pricing switch does now significantly slow down or speed up the issuance of pages.
137 | function testSwitchSmoothness() public {
138 | uint256 switchPageSaleTime = uint256(pages.getTargetSaleTime(8337e18) - pages.getTargetSaleTime(8336e18));
139 |
140 | assertRelApproxEq(
141 | uint256(pages.getTargetSaleTime(8336e18) - pages.getTargetSaleTime(8335e18)),
142 | switchPageSaleTime,
143 | 0.0005e18
144 | );
145 |
146 | assertRelApproxEq(
147 | switchPageSaleTime,
148 | uint256(pages.getTargetSaleTime(8338e18) - pages.getTargetSaleTime(8337e18)),
149 | 0.005e18
150 | );
151 | }
152 |
153 | /// @notice Test that page pricing matches expected behavior before switch.
154 | function testPagePricingPricingBeforeSwitch() public {
155 | // Expected sales rate according to mathematical formula.
156 | uint256 timeDelta = 60 days;
157 | uint256 numMint = 3572;
158 |
159 | vm.warp(block.timestamp + timeDelta);
160 |
161 | uint256 targetPrice = uint256(pages.targetPrice());
162 |
163 | for (uint256 i = 0; i < numMint; ++i) {
164 | uint256 price = pages.pagePrice();
165 | goo.mintForGobblers(user, price);
166 | vm.prank(user);
167 | pages.mintFromGoo(price, false);
168 | }
169 |
170 | uint256 finalPrice = pages.pagePrice();
171 |
172 | // If selling at target rate, final price should equal starting price.
173 | assertRelApproxEq(targetPrice, finalPrice, 0.01e18);
174 | }
175 |
176 | /// @notice Test that page pricing matches expected behavior after switch.
177 | function testPagePricingPricingAfterSwitch() public {
178 | uint256 timeDelta = 360 days;
179 | uint256 numMint = 9479;
180 |
181 | vm.warp(block.timestamp + timeDelta);
182 |
183 | uint256 targetPrice = uint256(pages.targetPrice());
184 |
185 | for (uint256 i = 0; i < numMint; ++i) {
186 | uint256 price = pages.pagePrice();
187 | goo.mintForGobblers(user, price);
188 | vm.prank(user);
189 | pages.mintFromGoo(price, false);
190 | }
191 |
192 | uint256 finalPrice = pages.pagePrice();
193 |
194 | // If selling at target rate, final price should equal starting price.
195 | assertRelApproxEq(finalPrice, targetPrice, 0.02e18);
196 | }
197 |
198 | function testInsufficientBalance() public {
199 | vm.prank(user);
200 | vm.expectRevert(stdError.arithmeticError);
201 | pages.mintFromGoo(type(uint256).max, false);
202 | }
203 |
204 | function testMintPriceExceededMax() public {
205 | uint256 cost = pages.pagePrice();
206 | goo.mintForGobblers(user, cost);
207 | vm.prank(user);
208 | vm.expectRevert(abi.encodeWithSelector(Pages.PriceExceededMax.selector, cost));
209 | pages.mintFromGoo(cost - 1, false);
210 | }
211 |
212 | /// @notice Mint a number of pages to the given address
213 | function mintPageToAddress(address addr, uint256 num) internal {
214 | for (uint256 i = 0; i < num; ++i) {
215 | goo.mintForGobblers(addr, pages.pagePrice());
216 |
217 | vm.prank(addr);
218 | pages.mintFromGoo(type(uint256).max, false);
219 | }
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/test/RandProvider.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
5 | import {Utilities} from "./utils/Utilities.sol";
6 | import {console} from "./utils/Console.sol";
7 | import {Vm} from "forge-std/Vm.sol";
8 | import {stdError} from "forge-std/Test.sol";
9 | import {ArtGobblers} from "../src/ArtGobblers.sol";
10 | import {Goo} from "../src/Goo.sol";
11 | import {Pages} from "../src/Pages.sol";
12 | import {GobblerReserve} from "../src/utils/GobblerReserve.sol";
13 | import {RandProvider} from "../src/utils/rand/RandProvider.sol";
14 | import {ChainlinkV1RandProvider} from "../src/utils/rand/ChainlinkV1RandProvider.sol";
15 | import {LinkToken} from "./utils/mocks/LinkToken.sol";
16 | import {VRFCoordinatorMock} from "chainlink/v0.8/mocks/VRFCoordinatorMock.sol";
17 | import {ERC721} from "solmate/tokens/ERC721.sol";
18 | import {LibString} from "solmate/utils/LibString.sol";
19 |
20 | /// @notice Unit test for the RandProvider contract.
21 | contract RandProviderTest is DSTestPlus {
22 | using LibString for uint256;
23 |
24 | Vm internal immutable vm = Vm(HEVM_ADDRESS);
25 |
26 | Utilities internal utils;
27 | address payable[] internal users;
28 |
29 | ArtGobblers internal gobblers;
30 | VRFCoordinatorMock internal vrfCoordinator;
31 | LinkToken internal linkToken;
32 | Goo internal goo;
33 | Pages internal pages;
34 | GobblerReserve internal team;
35 | GobblerReserve internal community;
36 | RandProvider internal randProvider;
37 |
38 | bytes32 private keyHash;
39 | uint256 private fee;
40 |
41 | uint256[] ids;
42 |
43 | //chainlink event
44 | event RandomnessRequest(address indexed sender, bytes32 indexed keyHash, uint256 indexed seed);
45 |
46 | /*//////////////////////////////////////////////////////////////
47 | SETUP
48 | //////////////////////////////////////////////////////////////*/
49 |
50 | function setUp() public {
51 | utils = new Utilities();
52 | users = utils.createUsers(5);
53 | linkToken = new LinkToken();
54 | vrfCoordinator = new VRFCoordinatorMock(address(linkToken));
55 |
56 | // Gobblers contract will be deployed after 4 contract deploys, and pages after 5.
57 | address gobblerAddress = utils.predictContractAddress(address(this), 4);
58 | address pagesAddress = utils.predictContractAddress(address(this), 5);
59 |
60 | team = new GobblerReserve(ArtGobblers(gobblerAddress), address(this));
61 | community = new GobblerReserve(ArtGobblers(gobblerAddress), address(this));
62 | randProvider = new ChainlinkV1RandProvider(
63 | ArtGobblers(gobblerAddress),
64 | address(vrfCoordinator),
65 | address(linkToken),
66 | keyHash,
67 | fee
68 | );
69 |
70 | goo = new Goo(
71 | // Gobblers:
72 | utils.predictContractAddress(address(this), 1),
73 | // Pages:
74 | utils.predictContractAddress(address(this), 2)
75 | );
76 |
77 | gobblers = new ArtGobblers(
78 | keccak256(abi.encodePacked(users[0])),
79 | block.timestamp,
80 | goo,
81 | Pages(pagesAddress),
82 | address(team),
83 | address(community),
84 | randProvider,
85 | "base",
86 | "",
87 | keccak256(abi.encodePacked("provenance"))
88 | );
89 |
90 | pages = new Pages(block.timestamp, goo, address(0xBEEF), gobblers, "");
91 | }
92 |
93 | function testRandomnessIsCorrectlyRequested() public {
94 | mintGobblerToAddress(users[0], 1);
95 | vm.warp(block.timestamp + 1 days);
96 |
97 | //we expect a randomnessRequest event to be emitted once the request reaches the VRFCoordinator.
98 | //we only check that the request comes from the correct address, i.e. the randProvider
99 | vm.expectEmit(true, false, false, false); // only check the first indexed event (sender address)
100 | emit RandomnessRequest(address(randProvider), 0, 0);
101 |
102 | gobblers.requestRandomSeed();
103 | }
104 |
105 | function testRandomnessIsFulfilled() public {
106 | //initially, randomness should be 0
107 | (uint64 randomSeed, , , , ) = gobblers.gobblerRevealsData();
108 | assertEq(randomSeed, 0);
109 | mintGobblerToAddress(users[0], 1);
110 | vm.warp(block.timestamp + 1 days);
111 | bytes32 requestId = gobblers.requestRandomSeed();
112 | uint256 randomness = uint256(keccak256(abi.encodePacked("seed")));
113 | vrfCoordinator.callBackWithRandomness(requestId, randomness, address(randProvider));
114 | //randomness from vrf should be set in gobblers contract
115 | (randomSeed, , , , ) = gobblers.gobblerRevealsData();
116 | assertEq(randomSeed, uint64(randomness));
117 | }
118 |
119 | function testOnlyGobblersCanRequestRandomness() public {
120 | vm.expectRevert(ChainlinkV1RandProvider.NotGobblers.selector);
121 | randProvider.requestRandomBytes();
122 | }
123 |
124 | function testRandomnessIsOnlyUpgradableByOwner() public {
125 | RandProvider newProvider = new ChainlinkV1RandProvider(ArtGobblers(address(0)), address(0), address(0), 0, 0);
126 | vm.expectRevert("UNAUTHORIZED");
127 | vm.prank(address(0xBEEFBABE));
128 | gobblers.upgradeRandProvider(newProvider);
129 | }
130 |
131 | function testRandomnessIsUpgradable() public {
132 | mintGobblerToAddress(users[0], 1);
133 | vm.warp(block.timestamp + 1 days);
134 | //initial address is correct
135 | assertEq(address(gobblers.randProvider()), address(randProvider));
136 |
137 | RandProvider newProvider = new ChainlinkV1RandProvider(ArtGobblers(address(0)), address(0), address(0), 0, 0);
138 | gobblers.upgradeRandProvider(newProvider);
139 | //final address is correct
140 | assertEq(address(gobblers.randProvider()), address(newProvider));
141 | }
142 |
143 | function testRandomnessIsResetWithPendingSeed() public {
144 | mintGobblerToAddress(users[0], 1);
145 | vm.warp(block.timestamp + 1 days);
146 | gobblers.requestRandomSeed();
147 | (, , , uint256 toBeRevealed, bool waiting) = gobblers.gobblerRevealsData();
148 | // Waiting for one gobbler to be revealed
149 | assertTrue(waiting);
150 | assertEq(toBeRevealed, 1);
151 |
152 | // Upgrade provider
153 | RandProvider newProvider = new ChainlinkV1RandProvider(
154 | gobblers,
155 | address(vrfCoordinator),
156 | address(linkToken),
157 | keyHash,
158 | fee
159 | );
160 | gobblers.upgradeRandProvider(newProvider);
161 |
162 | // State is reset
163 | (, , , toBeRevealed, waiting) = gobblers.gobblerRevealsData();
164 | assertFalse(waiting);
165 | assertEq(toBeRevealed, 0);
166 |
167 | // Randomness can still be fulfilled
168 | bytes32 requestId = gobblers.requestRandomSeed();
169 | (, , , toBeRevealed, waiting) = gobblers.gobblerRevealsData();
170 | assertTrue(waiting);
171 | assertEq(toBeRevealed, 1);
172 |
173 | uint256 randomness = uint256(keccak256(abi.encodePacked("seed")));
174 | vrfCoordinator.callBackWithRandomness(requestId, randomness, address(newProvider));
175 | //randomness from vrf should be set in gobblers contract
176 | (uint256 randomSeed, , , , ) = gobblers.gobblerRevealsData();
177 | assertEq(randomSeed, uint64(randomness));
178 | }
179 |
180 | /*//////////////////////////////////////////////////////////////
181 | HELPERS
182 | //////////////////////////////////////////////////////////////*/
183 |
184 | /// @notice Mint a number of gobblers to the given address
185 | function mintGobblerToAddress(address addr, uint256 num) internal {
186 | for (uint256 i = 0; i < num; ++i) {
187 | vm.startPrank(address(gobblers));
188 | goo.mintForGobblers(addr, gobblers.gobblerPrice());
189 | vm.stopPrank();
190 |
191 | vm.prank(addr);
192 | gobblers.mintFromGoo(type(uint256).max, false);
193 | }
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/test/VRGDAs.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
5 | import {Utilities} from "./utils/Utilities.sol";
6 | import {console} from "./utils/Console.sol";
7 | import {Vm} from "forge-std/Vm.sol";
8 | import {ArtGobblers} from "../src/ArtGobblers.sol";
9 | import {Goo} from "../src/Goo.sol";
10 | import {Pages} from "../src/Pages.sol";
11 | import {LinkToken} from "./utils/mocks/LinkToken.sol";
12 | import {VRFCoordinatorMock} from "chainlink/v0.8/mocks/VRFCoordinatorMock.sol";
13 | import {RandProvider} from "../src/utils/rand/RandProvider.sol";
14 | import {ChainlinkV1RandProvider} from "../src/utils/rand/ChainlinkV1RandProvider.sol";
15 | import {toDaysWadUnsafe} from "solmate/utils/SignedWadMath.sol";
16 |
17 | contract VRGDAsTest is DSTestPlus {
18 | Vm internal immutable vm = Vm(HEVM_ADDRESS);
19 |
20 | uint256 constant ONE_THOUSAND_YEARS = 356 days * 1000;
21 |
22 | Utilities internal utils;
23 | address payable[] internal users;
24 |
25 | ArtGobblers private gobblers;
26 | VRFCoordinatorMock private vrfCoordinator;
27 | LinkToken private linkToken;
28 |
29 | Goo goo;
30 | Pages pages;
31 | RandProvider randProvider;
32 |
33 | bytes32 private keyHash;
34 | uint256 private fee;
35 |
36 | function setUp() public {
37 | utils = new Utilities();
38 | users = utils.createUsers(5);
39 | linkToken = new LinkToken();
40 | vrfCoordinator = new VRFCoordinatorMock(address(linkToken));
41 |
42 | // Gobblers contract will be deployed after 2 contract deploys, and pages after 3.
43 | address gobblerAddress = utils.predictContractAddress(address(this), 2);
44 | address pagesAddress = utils.predictContractAddress(address(this), 3);
45 |
46 | randProvider = new ChainlinkV1RandProvider(
47 | ArtGobblers(gobblerAddress),
48 | address(vrfCoordinator),
49 | address(linkToken),
50 | keyHash,
51 | fee
52 | );
53 |
54 | goo = new Goo(gobblerAddress, pagesAddress);
55 |
56 | gobblers = new ArtGobblers(
57 | "root",
58 | block.timestamp,
59 | goo,
60 | Pages(pagesAddress),
61 | address(0xBEEF),
62 | address(0xBEEF),
63 | randProvider,
64 | "base",
65 | "",
66 | keccak256(abi.encodePacked("provenance"))
67 | );
68 |
69 | pages = new Pages(block.timestamp, goo, address(0xBEEF), gobblers, "");
70 | }
71 |
72 | // function testFindGobblerOverflowPoint() public view {
73 | // uint256 sold;
74 | // while (true) {
75 | // gobblers.getPrice(0 days, sold++);
76 | // }
77 | // }
78 |
79 | // function testFindPagesOverflowPoint() public view {
80 | // uint256 sold;
81 | // while (true) {
82 | // pages.getPrice(0 days, sold++);
83 | // }
84 | // }
85 |
86 | function testNoOverflowForMostGobblers(uint256 timeSinceStart, uint256 sold) public {
87 | gobblers.getVRGDAPrice(
88 | toDaysWadUnsafe(bound(timeSinceStart, 0 days, ONE_THOUSAND_YEARS)),
89 | bound(sold, 0, 1730)
90 | );
91 | }
92 |
93 | function testNoOverflowForAllGobblers(uint256 timeSinceStart, uint256 sold) public {
94 | gobblers.getVRGDAPrice(
95 | toDaysWadUnsafe(bound(timeSinceStart, 3870 days, ONE_THOUSAND_YEARS)),
96 | bound(sold, 0, 6391)
97 | );
98 | }
99 |
100 | function testFailOverflowForBeyondLimitGobblers(uint256 timeSinceStart, uint256 sold) public {
101 | gobblers.getVRGDAPrice(
102 | toDaysWadUnsafe(bound(timeSinceStart, 0 days, ONE_THOUSAND_YEARS)),
103 | bound(sold, 6392, type(uint128).max)
104 | );
105 | }
106 |
107 | function testGobblerPriceStrictlyIncreasesForMostGobblers() public {
108 | uint256 sold;
109 | uint256 previousPrice;
110 |
111 | while (sold <= 1730) {
112 | uint256 price = gobblers.getVRGDAPrice(0 days, sold++);
113 | assertGt(price, previousPrice);
114 | previousPrice = price;
115 | }
116 | }
117 |
118 | function testNoOverflowForFirst8465Pages(uint256 timeSinceStart, uint256 sold) public {
119 | pages.getVRGDAPrice(toDaysWadUnsafe(bound(timeSinceStart, 0 days, ONE_THOUSAND_YEARS)), bound(sold, 0, 8465));
120 | }
121 |
122 | function testPagePriceStrictlyIncreasesFor8465Pages() public {
123 | uint256 sold;
124 | uint256 previousPrice;
125 |
126 | while (sold <= 8465) {
127 | uint256 price = pages.getVRGDAPrice(0 days, sold++);
128 | assertGt(price, previousPrice);
129 | previousPrice = price;
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/test/correctness/GobblersCorrectness.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {Vm} from "forge-std/Vm.sol";
5 | import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
6 | import {LibString} from "solmate/utils/LibString.sol";
7 | import {ArtGobblers} from "../../src/ArtGobblers.sol";
8 | import {RandProvider} from "../../src/utils/rand/RandProvider.sol";
9 | import {toDaysWadUnsafe} from "solmate/utils/SignedWadMath.sol";
10 |
11 | import {Goo} from "../../src/Goo.sol";
12 | import {Pages} from "../../src/Pages.sol";
13 |
14 | contract GobblersCorrectnessTest is DSTestPlus {
15 | using LibString for uint256;
16 |
17 | uint256 internal immutable TWENTY_YEARS = 7300 days;
18 |
19 | uint256 internal MAX_MINTABLE;
20 |
21 | int256 internal LOGISTIC_SCALE;
22 |
23 | int256 internal immutable INITIAL_PRICE = 69.42e18;
24 |
25 | int256 internal immutable PER_PERIOD_PRICE_DECREASE = 0.31e18;
26 |
27 | int256 internal immutable TIME_SCALE = 0.0023e18;
28 |
29 | Vm internal immutable vm = Vm(HEVM_ADDRESS);
30 |
31 | ArtGobblers internal gobblers;
32 |
33 | function setUp() public {
34 | gobblers = new ArtGobblers(
35 | "root",
36 | block.timestamp,
37 | Goo(address(0)),
38 | Pages(address(0)),
39 | address(0),
40 | address(0),
41 | RandProvider(address(0)),
42 | "",
43 | "",
44 | keccak256(abi.encodePacked("provenance"))
45 | );
46 |
47 | MAX_MINTABLE = gobblers.MAX_MINTABLE();
48 | LOGISTIC_SCALE = int256((MAX_MINTABLE + 1) * 2e18);
49 | }
50 |
51 | function testFFICorrectness(uint256 timeSinceStart, uint256 numSold) public {
52 | // Limit num sold to max mint.
53 | numSold = bound(numSold, 0, MAX_MINTABLE);
54 |
55 | // Limit mint time to 20 years.
56 | timeSinceStart = bound(timeSinceStart, 0, TWENTY_YEARS);
57 |
58 | // Calculate actual price from VRGDA.
59 | try gobblers.getVRGDAPrice(toDaysWadUnsafe(timeSinceStart), numSold) returns (uint256 actualPrice) {
60 | // Calculate expected price from python script.
61 | uint256 expectedPrice = calculatePrice(
62 | timeSinceStart,
63 | numSold + 1,
64 | INITIAL_PRICE,
65 | PER_PERIOD_PRICE_DECREASE,
66 | LOGISTIC_SCALE,
67 | TIME_SCALE
68 | );
69 |
70 | if (expectedPrice < 0.0000000000001e18) return; // For really small prices we can't expect them to be equal.
71 |
72 | // Equal within 1 percent.
73 | assertRelApproxEq(actualPrice, expectedPrice, 0.01e18);
74 | } catch {
75 | // If it reverts that's fine, there are some bounds on the function, they are tested in VRGDAs.t.sol
76 | }
77 | }
78 |
79 | function calculatePrice(
80 | uint256 _timeSinceStart,
81 | uint256 _numSold,
82 | int256 _targetPrice,
83 | int256 _perPeriodPriceDecrease,
84 | int256 _logisticScale,
85 | int256 _timeScale
86 | ) private returns (uint256) {
87 | string[] memory inputs = new string[](15);
88 | inputs[0] = "python3";
89 | inputs[1] = "analysis/python/compute_price.py";
90 | inputs[2] = "gobblers";
91 | inputs[3] = "--time_since_start";
92 | inputs[4] = _timeSinceStart.toString();
93 | inputs[5] = "--num_sold";
94 | inputs[6] = _numSold.toString();
95 | inputs[7] = "--initial_price";
96 | inputs[8] = uint256(_targetPrice).toString();
97 | inputs[9] = "--per_period_price_decrease";
98 | inputs[10] = uint256(_perPeriodPriceDecrease).toString();
99 | inputs[11] = "--logistic_scale";
100 | inputs[12] = uint256(_logisticScale).toString();
101 | inputs[13] = "--time_scale";
102 | inputs[14] = uint256(_timeScale).toString();
103 |
104 | return abi.decode(vm.ffi(inputs), (uint256));
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/test/correctness/PagesCorrectness.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
5 | import {Vm} from "forge-std/Vm.sol";
6 | import {LibString} from "solmate/utils/LibString.sol";
7 | import {console} from "../utils/Console.sol";
8 | import {Pages} from "../../src/Pages.sol";
9 | import {ArtGobblers} from "../../src/ArtGobblers.sol";
10 | import {Goo} from "../../src/Goo.sol";
11 | import {toDaysWadUnsafe} from "solmate/utils/SignedWadMath.sol";
12 |
13 | contract PageCorrectnessTest is DSTestPlus {
14 | using LibString for uint256;
15 |
16 | Vm internal immutable vm = Vm(HEVM_ADDRESS);
17 |
18 | uint256 internal immutable TWENTY_YEARS = 7300 days;
19 |
20 | uint256 internal immutable MAX_MINTABLE = 9000;
21 |
22 | int256 internal LOGISTIC_SCALE;
23 |
24 | int256 internal immutable INITIAL_PRICE = 4.2069e18;
25 |
26 | int256 internal immutable PER_PERIOD_PRICE_DECREASE = 0.31e18;
27 |
28 | int256 internal immutable TIME_SCALE = 0.014e18;
29 |
30 | int256 internal immutable SWITCHOVER_TIME = 233e18;
31 |
32 | int256 internal immutable PER_PERIOD_POST_SWITCHOVER = 9e18;
33 |
34 | Pages internal pages;
35 |
36 | function setUp() public {
37 | pages = new Pages(block.timestamp, Goo(address(0)), address(0), ArtGobblers(address(0)), "");
38 |
39 | LOGISTIC_SCALE = int256((MAX_MINTABLE + 1) * 2e18);
40 | }
41 |
42 | function testFFICorrectness(uint256 timeSinceStart, uint256 numSold) public {
43 | // Limit num sold to max mint.
44 | numSold = bound(numSold, 0, 10000);
45 |
46 | // Limit mint time to 20 years.
47 | timeSinceStart = bound(timeSinceStart, 0, TWENTY_YEARS);
48 |
49 | // Calculate actual price from VRGDA.
50 | try pages.getVRGDAPrice(toDaysWadUnsafe(timeSinceStart), numSold) returns (uint256 actualPrice) {
51 | // Calculate expected price from python script.
52 | uint256 expectedPrice = calculatePrice(
53 | timeSinceStart,
54 | numSold + 1,
55 | INITIAL_PRICE,
56 | PER_PERIOD_PRICE_DECREASE,
57 | LOGISTIC_SCALE,
58 | TIME_SCALE,
59 | PER_PERIOD_POST_SWITCHOVER,
60 | SWITCHOVER_TIME
61 | );
62 |
63 | if (expectedPrice < 0.0000000000001e18) return; // For really small prices we can't expect them to be equal.
64 |
65 | // Equal within 1 percent.
66 | assertRelApproxEq(actualPrice, expectedPrice, 0.01e18);
67 | } catch {
68 | // If it reverts that's fine, there are some bounds on the function, they are tested in VRGDAs.t.sol
69 | }
70 | }
71 |
72 | function calculatePrice(
73 | uint256 _timeSinceStart,
74 | uint256 _numSold,
75 | int256 _targetPrice,
76 | int256 _PER_PERIOD_PRICE_DECREASE,
77 | int256 _logisticScale,
78 | int256 _timeScale,
79 | int256 _perPeriodPostSwitchover,
80 | int256 _switchoverTime
81 | ) private returns (uint256) {
82 | string[] memory inputs = new string[](19);
83 | inputs[0] = "python3";
84 | inputs[1] = "analysis/python/compute_price.py";
85 | inputs[2] = "pages";
86 | inputs[3] = "--time_since_start";
87 | inputs[4] = _timeSinceStart.toString();
88 | inputs[5] = "--num_sold";
89 | inputs[6] = _numSold.toString();
90 | inputs[7] = "--initial_price";
91 | inputs[8] = uint256(_targetPrice).toString();
92 | inputs[9] = "--per_period_price_decrease";
93 | inputs[10] = uint256(_PER_PERIOD_PRICE_DECREASE).toString();
94 | inputs[11] = "--logistic_scale";
95 | inputs[12] = uint256(_logisticScale).toString();
96 | inputs[13] = "--time_scale";
97 | inputs[14] = uint256(_timeScale).toString();
98 | inputs[15] = "--per_period_post_switchover";
99 | inputs[16] = uint256(_perPeriodPostSwitchover).toString();
100 | inputs[17] = "--switchover_time";
101 | inputs[18] = uint256(_switchoverTime).toString();
102 |
103 | return abi.decode(vm.ffi(inputs), (uint256));
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/test/deploy/DeployMainnet.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
5 | import {DeployMainnet} from "../../script/deploy/DeployMainnet.s.sol";
6 | import {Vm} from "forge-std/Vm.sol";
7 | import {console} from "forge-std/console.sol";
8 |
9 | import {Pages} from "../../src/Pages.sol";
10 | import {ArtGobblers} from "../../src/ArtGobblers.sol";
11 |
12 | contract DeployMainnetTest is DSTestPlus {
13 | Vm internal immutable vm = Vm(HEVM_ADDRESS);
14 |
15 | DeployMainnet deployScript;
16 |
17 | function setUp() public {
18 | vm.setEnv("DEPLOYER_PRIVATE_KEY", "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
19 | vm.setEnv("GOBBLER_PRIVATE_KEY", "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
20 | vm.setEnv("PAGES_PRIVATE_KEY", "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc");
21 | vm.setEnv("GOO_PRIVATE_KEY", "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd");
22 |
23 | vm.deal(vm.addr(vm.envUint("DEPLOYER_PRIVATE_KEY")), type(uint64).max);
24 |
25 | deployScript = new DeployMainnet();
26 | deployScript.run();
27 | }
28 |
29 | /// @notice Test goo addresses where correctly set.
30 | function testGooAddressCorrectness() public {
31 | assertEq(deployScript.goo().artGobblers(), address(deployScript.artGobblers()));
32 | assertEq(address(deployScript.goo().pages()), address(deployScript.pages()));
33 | }
34 |
35 | /// @notice Test page addresses where correctly set.
36 | function testPagesAddressCorrectness() public {
37 | assertEq(address(deployScript.pages().artGobblers()), address(deployScript.artGobblers()));
38 | assertEq(address(deployScript.pages().goo()), address(deployScript.goo()));
39 | }
40 |
41 | /// @notice Test that gobblers ownership is correctly transferred to governor.
42 | function testGobblerOwnership() public {
43 | assertEq(deployScript.artGobblers().owner(), deployScript.governorWallet());
44 | }
45 |
46 | /// @notice Test that merkle root is set correctly.
47 | function testRoot() public {
48 | assertEq(deployScript.root(), deployScript.artGobblers().merkleRoot());
49 | }
50 |
51 | /// @notice Test cold wallet was appropriately set.
52 | function testColdWallet() public {
53 | address coldWallet = deployScript.coldWallet();
54 | address communityOwner = deployScript.teamReserve().owner();
55 | address teamOwner = deployScript.communityReserve().owner();
56 | assertEq(coldWallet, communityOwner);
57 | assertEq(coldWallet, teamOwner);
58 | }
59 |
60 | /// @notice Test URIs are correctly set.
61 | function testURIs() public {
62 | ArtGobblers gobblers = deployScript.artGobblers();
63 | assertEq(gobblers.BASE_URI(), deployScript.gobblerBaseUri());
64 | assertEq(gobblers.UNREVEALED_URI(), deployScript.gobblerUnrevealedUri());
65 | Pages pages = deployScript.pages();
66 | assertEq(pages.BASE_URI(), deployScript.pagesBaseUri());
67 | }
68 |
69 | function testGobblerClaim() public {
70 | ArtGobblers gobblers = deployScript.artGobblers();
71 |
72 | // Address is in the merkle root.
73 | address minter = 0x0fb90B14e4BF3a2e5182B9b3cBD03e8d33b5b863;
74 |
75 | // Merkle proof.
76 | bytes32[] memory proof = new bytes32[](11);
77 | proof[0] = 0x541a56539b694a70dde9dabe952bb520f496fce67614316102d0a842d3615f2a;
78 | proof[1] = 0x48b4e269c7ce862127a0acc74a4ea667571fc3d7794d3c738ba5012ab356e1bd;
79 | proof[2] = 0x44ede3b0062acbd441c2862a9dbfbef56939941b10f3dfd5681e352e433a40ba;
80 | proof[3] = 0xbbbdd1b0ab9aade132a0d46f55f9a6b9aa4cc36e40eaca0c0edde920dfd10352;
81 | proof[4] = 0x40696f4fa548ba37ba76376a7e1d537794ef7c76beedb45bf2e67d83b91fb35d;
82 | proof[5] = 0x10ecbfee943986149ef31225bd2da45c2f0d1c7aaebb6c9fb66a938e90d57995;
83 | proof[6] = 0x8ed4b1f65bacc0c3374030b948d54004b636896390fa8ade8e81dec61b382231;
84 | proof[7] = 0xcd29788189153cafa66cb771589e5211d6c0418de49b25685b5d678ed136ad1d;
85 | proof[8] = 0xeffb064155d13bc87b27f9f78d811053863836c89e49c6f96f3856b0144370ee;
86 | proof[9] = 0x7413ded58393d42ce39eaedd07d8b57f62e5c068d5608300cc7cccd96ca40380;
87 | proof[10] = 0xf3927c3b5a5dcce415463d504510cc3a3da57a48199a96f49e0257e2cd66d3a5;
88 |
89 | // Initial balance should be zero.
90 | assertEq(gobblers.balanceOf(minter), 0);
91 |
92 | // Move time and mint.
93 | vm.warp(gobblers.mintStart());
94 | vm.prank(minter);
95 | gobblers.claimGobbler(proof);
96 |
97 | // Check that balance has increased.
98 | assertEq(gobblers.balanceOf(minter), 1);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/test/deploy/DeployRinkeby.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol";
5 | import {DeployRinkeby} from "../../script/deploy/DeployRinkeby.s.sol";
6 | import {Vm} from "forge-std/Vm.sol";
7 | import {console} from "forge-std/console.sol";
8 |
9 | import {Pages} from "../../src/Pages.sol";
10 | import {ArtGobblers} from "../../src/ArtGobblers.sol";
11 |
12 | contract DeployRinkebyTest is DSTestPlus {
13 | Vm internal immutable vm = Vm(HEVM_ADDRESS);
14 |
15 | DeployRinkeby deployScript;
16 |
17 | function setUp() public {
18 | vm.setEnv("DEPLOYER_PRIVATE_KEY", "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
19 | vm.setEnv("GOBBLER_PRIVATE_KEY", "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
20 | vm.setEnv("PAGES_PRIVATE_KEY", "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc");
21 | vm.setEnv("GOO_PRIVATE_KEY", "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd");
22 |
23 | vm.deal(vm.addr(vm.envUint("DEPLOYER_PRIVATE_KEY")), type(uint64).max);
24 |
25 | deployScript = new DeployRinkeby();
26 | deployScript.run();
27 | }
28 |
29 | /// @notice Test goo addresses where correctly set.
30 | function testGooAddressCorrectness() public {
31 | assertEq(deployScript.goo().artGobblers(), address(deployScript.artGobblers()));
32 | assertEq(address(deployScript.goo().pages()), address(deployScript.pages()));
33 | }
34 |
35 | /// @notice Test page addresses where correctly set.
36 | function testPagesAddressCorrectness() public {
37 | assertEq(address(deployScript.pages().artGobblers()), address(deployScript.artGobblers()));
38 | assertEq(address(deployScript.pages().goo()), address(deployScript.goo()));
39 | }
40 |
41 | /// @notice Test that merkle root was correctly set.
42 | function testMerkleRoot() public {
43 | vm.warp(deployScript.mintStart());
44 | // Use merkle root as user to test simple proof.
45 | address user = deployScript.root();
46 | bytes32[] memory proof;
47 | ArtGobblers gobblers = deployScript.artGobblers();
48 | vm.prank(user);
49 | gobblers.claimGobbler(proof);
50 | // Verify gobbler ownership.
51 | assertEq(gobblers.ownerOf(1), user);
52 | }
53 |
54 | /// @notice Test cold wallet was appropriately set.
55 | function testColdWallet() public {
56 | address coldWallet = deployScript.coldWallet();
57 | address communityOwner = deployScript.teamReserve().owner();
58 | address teamOwner = deployScript.communityReserve().owner();
59 | assertEq(coldWallet, communityOwner);
60 | assertEq(coldWallet, teamOwner);
61 | }
62 |
63 | /// @notice Test URIs are correctly set.
64 | function testURIs() public {
65 | ArtGobblers gobblers = deployScript.artGobblers();
66 | assertEq(gobblers.BASE_URI(), deployScript.gobblerBaseUri());
67 | assertEq(gobblers.UNREVEALED_URI(), deployScript.gobblerUnrevealedUri());
68 | Pages pages = deployScript.pages();
69 | assertEq(pages.BASE_URI(), deployScript.pagesBaseUri());
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/test/utils/LibRLP.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {Bytes32AddressLib} from "solmate/utils/Bytes32AddressLib.sol";
5 |
6 | library LibRLP {
7 | using Bytes32AddressLib for bytes32;
8 |
9 | // prettier-ignore
10 | function computeAddress(address deployer, uint256 nonce) internal pure returns (address) {
11 | // The integer zero is treated as an empty byte string, and as a result it only has a length prefix, 0x80, computed via 0x80 + 0.
12 | // A one byte integer uses its own value as its length prefix, there is no additional "0x80 + length" prefix that comes before it.
13 | if (nonce == 0x00) return keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), deployer, bytes1(0x80))).fromLast20Bytes();
14 | if (nonce <= 0x7f) return keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), deployer, uint8(nonce))).fromLast20Bytes();
15 |
16 | // Nonces greater than 1 byte all follow a consistent encoding scheme, where each value is preceded by a prefix of 0x80 + length.
17 | if (nonce <= type(uint8).max) return keccak256(abi.encodePacked(bytes1(0xd7), bytes1(0x94), deployer, bytes1(0x81), uint8(nonce))).fromLast20Bytes();
18 | if (nonce <= type(uint16).max) return keccak256(abi.encodePacked(bytes1(0xd8), bytes1(0x94), deployer, bytes1(0x82), uint16(nonce))).fromLast20Bytes();
19 | if (nonce <= type(uint24).max) return keccak256(abi.encodePacked(bytes1(0xd9), bytes1(0x94), deployer, bytes1(0x83), uint24(nonce))).fromLast20Bytes();
20 |
21 | // More details about RLP encoding can be found here: https://eth.wiki/fundamentals/rlp
22 | // 0xda = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x84 ++ nonce)
23 | // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex)
24 | // 0x84 = 0x80 + 0x04 (0x04 = the bytes length of the nonce, 4 bytes, in hex)
25 | // We assume nobody can have a nonce large enough to require more than 32 bytes.
26 | return keccak256(abi.encodePacked(bytes1(0xda), bytes1(0x94), deployer, bytes1(0x84), uint32(nonce))).fromLast20Bytes();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/test/utils/Utilities.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {DSTest} from "ds-test/test.sol";
5 | import {Vm} from "forge-std/Vm.sol";
6 |
7 | import {LibRLP} from "./LibRLP.sol";
8 |
9 | // common utilities for forge tests
10 | contract Utilities is DSTest {
11 | Vm internal immutable vm = Vm(HEVM_ADDRESS);
12 | bytes32 internal nextUser = keccak256(abi.encodePacked("user address"));
13 |
14 | function getNextUserAddress() external returns (address payable) {
15 | // bytes32 to address conversion
16 | address payable user = payable(address(uint160(uint256(nextUser))));
17 | nextUser = keccak256(abi.encodePacked(nextUser));
18 | return user;
19 | }
20 |
21 | // create users with 100 ether balance
22 | function createUsers(uint256 userNum) external returns (address payable[] memory) {
23 | address payable[] memory users = new address payable[](userNum);
24 | for (uint256 i = 0; i < userNum; ++i) {
25 | address payable user = this.getNextUserAddress();
26 | vm.deal(user, 100 ether);
27 | users[i] = user;
28 | }
29 | return users;
30 | }
31 |
32 | // move block.number forward by a given number of blocks
33 | function mineBlocks(uint256 numBlocks) external {
34 | uint256 targetBlock = block.number + numBlocks;
35 | vm.roll(targetBlock);
36 | }
37 |
38 | function predictContractAddress(address user, uint256 distanceFromCurrentNonce) external returns (address) {
39 | return LibRLP.computeAddress(user, vm.getNonce(user) + distanceFromCurrentNonce);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/test/utils/mocks/LinkToken.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 |
3 | // adapter from dapptools-starter-kit
4 | pragma solidity ^0.8.0;
5 |
6 | import {ERC20} from "solmate/tokens/ERC20.sol";
7 |
8 | interface ERC677Receiver {
9 | function onTokenTransfer(
10 | address _sender,
11 | uint256 _value,
12 | bytes memory _data
13 | ) external;
14 | }
15 |
16 | contract LinkToken is ERC20 {
17 | uint256 initialSupply = 1000000000000000000000000;
18 |
19 | constructor() ERC20("LinkToken", "LINK", 18) {
20 | _mint(msg.sender, initialSupply);
21 | }
22 |
23 | event Transfer(address indexed from, address indexed to, uint256 value, bytes data);
24 |
25 | /**
26 | * @dev transfer token to a contract address with additional data if the recipient is a contact.
27 | * @param _to The address to transfer to.
28 | * @param _value The amount to be transferred.
29 | * @param _data The extra data to be passed to the receiving contract.
30 | */
31 | function transferAndCall(
32 | address _to,
33 | uint256 _value,
34 | bytes memory _data
35 | ) public virtual returns (bool success) {
36 | super.transfer(_to, _value);
37 | // emit Transfer(msg.sender, _to, _value, _data);
38 | emit Transfer(msg.sender, _to, _value, _data);
39 | if (isContract(_to)) {
40 | contractFallback(_to, _value, _data);
41 | }
42 | return true;
43 | }
44 |
45 | // PRIVATE
46 |
47 | function contractFallback(
48 | address _to,
49 | uint256 _value,
50 | bytes memory _data
51 | ) private {
52 | ERC677Receiver receiver = ERC677Receiver(_to);
53 | receiver.onTokenTransfer(msg.sender, _value, _data);
54 | }
55 |
56 | function isContract(address _addr) private view returns (bool hasCode) {
57 | uint256 length;
58 | assembly {
59 | length := extcodesize(_addr)
60 | }
61 | return length > 0;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/test/utils/mocks/MockGooCalculator.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity >=0.8.0;
3 |
4 | import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol";
5 |
6 | contract MockGooCalculator {
7 | using FixedPointMathLib for uint256;
8 |
9 | /// @notice Compute goo balance based on emission multiple, last balance, and days elapsed.
10 | /// @dev Must be kept up to date with the gooBalance function's corresponding emission balance calculations in ArtGobblers.sol.
11 | /// @dev Forked from https://github.com/artgobblers/art-gobblers/blob/2f19bc901ed2f1bfedeb6f113b073bfc3585386a/src/ArtGobblers.sol#L693-L708
12 | function computeGooBalance(
13 | uint256 emissionMultiple,
14 | uint256 lastBalanceWad,
15 | uint256 daysElapsedWad
16 | ) public pure returns (uint256) {
17 | unchecked {
18 | uint256 daysElapsedSquaredWad = daysElapsedWad.mulWadDown(daysElapsedWad); // Need to use wad math here.
19 |
20 | // prettier-ignore
21 | return lastBalanceWad + // The last recorded balance.
22 |
23 | // Don't need to do wad multiplication since we're
24 | // multiplying by a plain integer with no decimals.
25 | // Shift right by 2 is equivalent to division by 4.
26 | ((emissionMultiple * daysElapsedSquaredWad) >> 2) +
27 |
28 | daysElapsedWad.mulWadDown( // Terms are wads, so must mulWad.
29 | // No wad multiplication for emissionMultiple * lastBalance
30 | // because emissionMultiple is a plain integer with no decimals.
31 | // We multiply the sqrt's radicand by 1e18 because it expects ints.
32 | (emissionMultiple * lastBalanceWad * 1e18).sqrt()
33 | );
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------