├── .env_example ├── .gitattributes ├── .github ├── CODEOWNERS └── workflows │ └── quality-assessment.yml ├── .gitignore ├── .gitmodules ├── .hintrc ├── .solhint.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── abi ├── common │ ├── BaseERC721A.json │ ├── BoostPXP.json │ ├── Energy.json │ ├── LevelUp.json │ ├── NonceRegistry.json │ ├── Rewards.json │ ├── Signer.json │ └── Treasury.json ├── pookyball │ ├── Pookyball.json │ ├── PookyballAscension.json │ ├── PookyballLevelUp.json │ ├── PookyballReroll.json │ ├── Pressure.json │ └── RefillableSale.json ├── stickers │ ├── Stickers.json │ ├── StickersAscension.json │ ├── StickersController.json │ ├── StickersLevelUp.json │ ├── StickersManager.json │ └── StickersSale.json └── tokens │ └── POK.json ├── bytecode ├── common │ ├── BaseERC721A.bin │ ├── BoostPXP.bin │ ├── Energy.bin │ ├── LevelUp.bin │ ├── NonceRegistry.bin │ ├── Rewards.bin │ ├── Signer.bin │ └── Treasury.bin ├── pookyball │ ├── Pookyball.bin │ ├── PookyballAscension.bin │ ├── PookyballLevelUp.bin │ ├── PookyballReroll.bin │ ├── Pressure.bin │ └── RefillableSale.bin ├── stickers │ ├── Stickers.bin │ ├── StickersAscension.bin │ ├── StickersController.bin │ ├── StickersLevelUp.bin │ ├── StickersManager.bin │ └── StickersSale.bin └── tokens │ └── POK.bin ├── codecov.yml ├── docs ├── alchemy_webhooks │ ├── energy.md │ ├── pookyball.md │ ├── pookyball_ascension.md │ ├── pookyball_pressure.md │ ├── pookyball_reroll.md │ ├── rewards.md │ ├── stickers.md │ ├── stickers_ascension.md │ └── stickers_controller.md ├── chains.md ├── codebase.md ├── stickers.md └── wallet.md ├── flattened ├── BoostPXP.sol ├── Energy.sol ├── NonceRegistry.sol ├── POK.sol ├── Pookyball.sol ├── PookyballAscension.sol ├── PookyballLevelUp.sol ├── PookyballReroll.sol ├── Pressure.sol ├── RefillableSale.sol ├── Rewards.sol ├── Stickers.sol ├── StickersAscension.sol ├── StickersController.sol ├── StickersLevelUp.sol ├── StickersManager.sol └── StickersSale.sol ├── foundry.toml ├── lefthook.yml ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── remappings.txt ├── script ├── amoy │ ├── 01_POK.s.sol │ ├── 02_Energy.s.sol │ ├── 03_NonceRegistry.s.sol │ ├── 04_BoostPXP.s.sol │ ├── 05_Pookyball.s.sol │ ├── 06_PookyballLevelUp.s.sol │ ├── 07_RefillableSale.s.sol │ ├── 08_Pressure.s.sol │ ├── 09_PookyballReroll.s.sol │ ├── 09_Stickers.s.sol │ ├── 12_StickersLevelUp.s.sol │ ├── 13_StickersSale.s.sol │ ├── 14_StickersController.s.sol │ ├── 15_StickersManager.s.sol │ ├── 16_StickersAscension.s.sol │ ├── 17_PookyballAscension.s.sol │ └── 18_Rewards.s.sol ├── artifacts.ts ├── comments.ts ├── polygon │ ├── 01_Stickers.s.sol │ ├── 02_PookyballAscension.s.sol │ ├── 03_PookyballLevelUp.sol │ ├── 04_PookyballReroll.sol │ └── 06_StickersAscension.s.sol └── shell │ ├── deploy.sh │ └── flatten.sh ├── src ├── common │ ├── BaseERC721A.sol │ ├── BoostPXP.sol │ ├── Energy.sol │ ├── IBaseERC721A.sol │ ├── INonceRegistry.sol │ ├── ITreasury.sol │ ├── LevelUp.sol │ ├── NonceRegistry.sol │ ├── Rewards.sol │ ├── Signer.sol │ └── Treasury.sol ├── pookyball │ ├── IPookyball.sol │ ├── Pookyball.sol │ ├── PookyballAscension.sol │ ├── PookyballLevelUp.sol │ ├── PookyballReroll.sol │ ├── Pressure.sol │ └── RefillableSale.sol ├── stickers │ ├── IStickers.sol │ ├── IStickersController.sol │ ├── Stickers.sol │ ├── StickersAscension.sol │ ├── StickersController.sol │ ├── StickersLevelUp.sol │ ├── StickersManager.sol │ └── StickersSale.sol ├── tokens │ ├── IPOK.sol │ └── POK.sol └── types │ └── VRFConfig.sol ├── test ├── BaseTest.sol ├── common │ ├── Energy.t.sol │ ├── NonceRegistry.t.sol │ ├── Rewards.fuzz.t.sol │ └── Rewards.t.sol ├── datasets │ ├── PookyballLevelUp_pricing.json │ ├── PookyballLevelUp_slots.json │ ├── StickersLevelUp_pricing.json │ └── StickersLevelUp_slots.json ├── pookyball │ ├── Pookyball.t.sol │ ├── PookyballAscension.t.sol │ ├── PookyballLevelUp.t.sol │ ├── PookyballReroll.t.sol │ ├── Pressure.t.sol │ └── RefillableSale.t.sol ├── setup │ ├── LevelUpSetup.sol │ ├── POKSetup.sol │ ├── PookyballSetup.sol │ ├── RewardsSetup.sol │ ├── StickersControllerSetup.sol │ ├── StickersSetup.sol │ └── VRFCoordinatorV2Setup.sol ├── stickers │ ├── Stickers.t.sol │ ├── StickersAscension.t.sol │ ├── StickersController.t.sol │ ├── StickersLevelUp.fuzz.t.sol │ ├── StickersLevelUp.t.sol │ ├── StickersManager.t.sol │ └── StickersSale.t.sol ├── tokens │ └── POK.t.sol └── utils │ ├── AccessControlAssertions.sol │ └── InvalidReceiver.sol └── tsconfig.json /.env_example: -------------------------------------------------------------------------------- 1 | RPC_URL=https://rpc-amoy.polygon.technology/ 2 | CHAIN_ID=80002 3 | 4 | SALT=0x1234567890abcdef 5 | 6 | DEPLOYER_ADDRESS=0xC0DE54132cA2B4BAce7C02AF31Aa9d3c1905f379 7 | DEPLOYER_PK= 8 | 9 | ADMIN_ADDRESS=0xF00Db2f08D1F6b3f6089573085B5826Bb358e319 10 | ADMIN_PK= 11 | 12 | OPERATOR_ADDRESS=0x481074326aC46C7BC52f0b25D2F7Aaf40f586472 13 | 14 | BACKEND_ADDRESS=0xCAFE3e690bf74Ec274210E1c448130c1f8228513 15 | 16 | TREASURY_ADDRESS=0xBABA035d2e22073C3a2AadA404dae4f6A9D57BD7 17 | 18 | ROYALTY_RECEIVER_ADDRESS=0x2dfCa6e357a73D180B8e6aa8f7690A315a4395F7 19 | 20 | VRF_COORDINATOR_ADDRESS=0x7a1BaC17Ccc5b313516C5E16fb24f7659aA5ebed 21 | VRF_KEY_HASH=0x4b09e658ed251bcafeebbc69400383d49f344ace09b9576fe248bb02c003fe9f 22 | VRF_SUB_ID=2307 23 | VRF_MINIMUM_REQUEST_CONFIRMATIONS=10 24 | VRF_CALLBACK_GAS_LIMIT=2500000 25 | 26 | POLYGONSCAN_API_KEY= 27 | 28 | POOKYBALL_BASE_URI=https://tokens.pooky.tech/ 29 | POOKYBALL_CONTRACT_URI=https://static.pooky.tech/contracts/Pookyball.json 30 | 31 | CONTRACT_POK_ADDRESS=0x66e4E3d15F14DEEE674e44666A60aC9D43CF3D1A 32 | CONTRACT_ENERGY_ADDRESS=0xcE3bAF0c9989beC8e39B0D063Ffdf5C09373a510 33 | CONTRACT_NONCE_REGISTRY_ADDRESS=0x2787DA764ebfaB0766f8737CA2Ce849Ea1c6fD88 34 | CONTRACT_BOOST_PXP_ADDRESS=0xd2141454Ba77aEf88FF7dD865334abaf8eFa83C3 35 | CONTRACT_POOKYBALL_ADDRESS=0x91290239CD991eB8B748FaFe3fb2401C5ec3c988 36 | CONTRACT_POOKYBALL_LEVEL_UP_ADDRESS=0xEd78B7932d7fb92b28436de95B64C5C12DdD23a7 37 | CONTRACT_POOKYBALL_REFIILABLE_SALE_ADDRESS=0xee62bA98Deee1cFfa61F1AA07329c2d5fcC8ea17 38 | CONTRACT_PRESSURE_ADDRESS=0x3876E098488092af30aFACBF4e99553372CaAedD 39 | CONTRACT_POOKYBALL_REROLL_ADDRESS=0x5c2E6a21813751411c4d445F9DFa2638c8271f61 40 | CONTRACT_STICKERS_ADDRESS=0x975d59D5ff4c1D4ED908892377E23Bf00c40f7aD 41 | CONTRACT_STICKERS_LEVEL_UP_ADDRESS=0x92ddB44c3D33Ef49F0f8EeE3ca6F1bD07e4188C4 42 | CONTRACT_STICKERS_SALE_ADDRESS=0xa3C4BD44E80cD3E83BCBB33EB6C3B3Eb24286073 43 | CONTRACT_STICKERS_CONTROLLER_ADDRESS=0xB9b85d3DCF4bB8c064D9F594354F07F5DCE12daF 44 | CONTRACT_STICKERS_MANAGER_ADDRESS=0x17FBe0246aC8F13D457dDcc2b46FBCD6E80eA675 45 | CONTRACT_STICKERS_ASCENSION=0xCb20FEe5113F10fE06BD9452d2E5cD1972161006 46 | CONTRACT_POOKYBALL_ASCENSION_ADDRESS=0x1369B4e6B4B1A8f1a6C3c79dCB5DbB251EdC0D30 47 | CONTRACT_REWARDS=0xBa609c15697617f9a243A5E0A609371044497719 48 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | abi/** linguist-generated=true 2 | bytecode/** linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mathieu-bour 2 | -------------------------------------------------------------------------------- /.github/workflows/quality-assessment.yml: -------------------------------------------------------------------------------- 1 | name: Quality assessment 2 | on: 3 | push: 4 | branches: [main] 5 | tags: ['v*'] 6 | pull_request: 7 | types: [opened, reopened, synchronize] 8 | jobs: 9 | lint: 10 | name: Lint with Solhint 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: Setup environment 14 | uses: pooky-labs/actions/setup-foundry@main 15 | - name: Run Solhint 16 | run: pnpm lint 17 | 18 | fuzz: 19 | name: Test with intense fuzzing 20 | runs-on: ubuntu-22.04 21 | steps: 22 | - name: Setup environment 23 | uses: pooky-labs/actions/setup-foundry@main 24 | - name: Run fuzz testing 25 | run: forge test 26 | env: 27 | FOUNDRY_PROFILE: intense 28 | 29 | snapshot: 30 | name: Compute the gas snapshot 31 | runs-on: ubuntu-22.04 32 | steps: 33 | - name: Setup environment 34 | uses: pooky-labs/actions/setup-foundry@main 35 | - name: Run snapshot 36 | run: NO_COLOR=1 forge snapshot >> $GITHUB_STEP_SUMMARY 37 | 38 | coverage: 39 | name: Compute the coverage with the tests 40 | runs-on: ubuntu-22.04 41 | steps: 42 | - name: Setup environment 43 | uses: pooky-labs/actions/setup-foundry@main 44 | - name: Run tests with coverage 45 | run: forge coverage --report=lcov -vvv 46 | - name: Send the coverage results to Codecov 47 | uses: codecov/codecov-action@v3 48 | with: 49 | token: ${{ secrets.CODECOV_TOKEN }} 50 | files: ./lcov.info 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NodeJS files 2 | node_modules/ 3 | coverage 4 | coverage.json 5 | typechain 6 | 7 | # Foundry files 8 | cache/ 9 | artifacts/ 10 | cache/ 11 | out/ 12 | 13 | # Ignore Environment Variables! 14 | .env 15 | .env.* 16 | 17 | # IDEs (JetBrains and VSCode) 18 | .idea/ 19 | .vscode/ 20 | .history/ 21 | 22 | # Ignore flattened files 23 | flattened.txt 24 | 25 | broadcast 26 | 27 | # Foundry Coverage / gas 28 | lcov.info 29 | .gas-snapshot 30 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts"] 5 | path = lib/openzeppelin-contracts 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 7 | branch = release-v4.9 8 | [submodule "lib/operator-filter-registry"] 9 | path = lib/operator-filter-registry 10 | url = https://github.com/ProjectOpenSea/operator-filter-registry 11 | [submodule "lib/chainlink"] 12 | path = lib/chainlink 13 | url = https://github.com/smartcontractkit/chainlink 14 | [submodule "lib/ERC721A"] 15 | path = lib/ERC721A 16 | url = https://github.com/chiru-labs/ERC721A 17 | [submodule "lib/solady"] 18 | path = lib/solady 19 | url = https://github.com/Vectorized/solady 20 | -------------------------------------------------------------------------------- /.hintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "development" 4 | ], 5 | "hints": { 6 | "typescript-config/consistent-casing": "off" 7 | } 8 | } -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:default", 3 | "rules": { 4 | "max-line-length": ["error", 140], 5 | "func-name-mixedcase": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | Thanks for considering contributing to Pooky smart contracts! 4 | This document briefly describes how we have organized the [pooky-labs/smart-contracts](https://github.com/pooky-labs/smart-contracts) repository. 5 | 6 | This repository uses the following environment: 7 | 8 | - [Foundry](https://book.getfoundry.sh/) 9 | - Node.js LTS (18.x since October 2022) 10 | - pnpm 7.x ([learn how to install pnpm](https://pnpm.io/installation)) 11 | 12 | ## Repository organization 13 | 14 | We use [Foundry](https://book.getfoundry.sh/) as our main framework for developing our smart contracts. 15 | Our smart contracts repository is based on a standard Foundry architecture. 16 | 17 | ``` 18 | . 19 | ├── abi # Pooky contracts ABIs 20 | ├── artifacts # (git-ignored) Foundry compile artifacts 21 | ├── cache # (git-ignored) Foundry cache 22 | ├── coverage # (git-ignored) Code-coverage reports 23 | ├── lib # Foundry dependencies 24 | ├── node_modules # (git-ignored) Project Node dependencies 25 | ├── script # Runnable Foundry scripts 26 | ├── src # Smart contracts Solidity source code 27 | └── test # Smart contracts tests 28 | ``` 29 | 30 | ## Testing 31 | 32 | Contract unit tests are located inside the [`test/`](./test) directory. 33 | Each contract has it own test file that contains its unit tests. 34 | 35 | ``` 36 | src/tokens/POK.sol => test/tokens/POK.t.sol 37 | src/tokens/Pookyball.sol => test/tokens/Pookyball.t.sol 38 | etc. 39 | ``` 40 | 41 | ## Useful resources 42 | 43 | - [Foundry Book](https://book.getfoundry.sh/) 44 | - [OpenZeppelin Contracts 4.x](https://docs.openzeppelin.com/contracts/4.x/) 45 | - [Polygon Amoy Testnet Faucet](https://faucet.polygon.technology/) 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 Pooky Labs Ltd. 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. 22 | -------------------------------------------------------------------------------- /abi/common/Treasury.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "function", 4 | "name": "cancelOwnershipHandover", 5 | "inputs": [], 6 | "outputs": [], 7 | "stateMutability": "payable" 8 | }, 9 | { 10 | "type": "function", 11 | "name": "changeTreasury", 12 | "inputs": [ 13 | { 14 | "name": "_treasury", 15 | "type": "address", 16 | "internalType": "address" 17 | } 18 | ], 19 | "outputs": [], 20 | "stateMutability": "nonpayable" 21 | }, 22 | { 23 | "type": "function", 24 | "name": "completeOwnershipHandover", 25 | "inputs": [ 26 | { 27 | "name": "pendingOwner", 28 | "type": "address", 29 | "internalType": "address" 30 | } 31 | ], 32 | "outputs": [], 33 | "stateMutability": "payable" 34 | }, 35 | { 36 | "type": "function", 37 | "name": "owner", 38 | "inputs": [], 39 | "outputs": [ 40 | { 41 | "name": "result", 42 | "type": "address", 43 | "internalType": "address" 44 | } 45 | ], 46 | "stateMutability": "view" 47 | }, 48 | { 49 | "type": "function", 50 | "name": "ownershipHandoverExpiresAt", 51 | "inputs": [ 52 | { 53 | "name": "pendingOwner", 54 | "type": "address", 55 | "internalType": "address" 56 | } 57 | ], 58 | "outputs": [ 59 | { 60 | "name": "result", 61 | "type": "uint256", 62 | "internalType": "uint256" 63 | } 64 | ], 65 | "stateMutability": "view" 66 | }, 67 | { 68 | "type": "function", 69 | "name": "renounceOwnership", 70 | "inputs": [], 71 | "outputs": [], 72 | "stateMutability": "payable" 73 | }, 74 | { 75 | "type": "function", 76 | "name": "requestOwnershipHandover", 77 | "inputs": [], 78 | "outputs": [], 79 | "stateMutability": "payable" 80 | }, 81 | { 82 | "type": "function", 83 | "name": "transferOwnership", 84 | "inputs": [ 85 | { 86 | "name": "newOwner", 87 | "type": "address", 88 | "internalType": "address" 89 | } 90 | ], 91 | "outputs": [], 92 | "stateMutability": "payable" 93 | }, 94 | { 95 | "type": "function", 96 | "name": "treasury", 97 | "inputs": [], 98 | "outputs": [ 99 | { 100 | "name": "", 101 | "type": "address", 102 | "internalType": "address" 103 | } 104 | ], 105 | "stateMutability": "view" 106 | }, 107 | { 108 | "type": "event", 109 | "name": "OwnershipHandoverCanceled", 110 | "inputs": [ 111 | { 112 | "name": "pendingOwner", 113 | "type": "address", 114 | "indexed": true, 115 | "internalType": "address" 116 | } 117 | ], 118 | "anonymous": false 119 | }, 120 | { 121 | "type": "event", 122 | "name": "OwnershipHandoverRequested", 123 | "inputs": [ 124 | { 125 | "name": "pendingOwner", 126 | "type": "address", 127 | "indexed": true, 128 | "internalType": "address" 129 | } 130 | ], 131 | "anonymous": false 132 | }, 133 | { 134 | "type": "event", 135 | "name": "OwnershipTransferred", 136 | "inputs": [ 137 | { 138 | "name": "oldOwner", 139 | "type": "address", 140 | "indexed": true, 141 | "internalType": "address" 142 | }, 143 | { 144 | "name": "newOwner", 145 | "type": "address", 146 | "indexed": true, 147 | "internalType": "address" 148 | } 149 | ], 150 | "anonymous": false 151 | }, 152 | { 153 | "type": "error", 154 | "name": "InsufficientValue", 155 | "inputs": [ 156 | { 157 | "name": "expected", 158 | "type": "uint256", 159 | "internalType": "uint256" 160 | }, 161 | { 162 | "name": "actual", 163 | "type": "uint256", 164 | "internalType": "uint256" 165 | } 166 | ] 167 | }, 168 | { 169 | "type": "error", 170 | "name": "NewOwnerIsZeroAddress", 171 | "inputs": [] 172 | }, 173 | { 174 | "type": "error", 175 | "name": "NoHandoverRequest", 176 | "inputs": [] 177 | }, 178 | { 179 | "type": "error", 180 | "name": "TransferFailed", 181 | "inputs": [ 182 | { 183 | "name": "recipient", 184 | "type": "address", 185 | "internalType": "address" 186 | }, 187 | { 188 | "name": "amount", 189 | "type": "uint256", 190 | "internalType": "uint256" 191 | } 192 | ] 193 | }, 194 | { 195 | "type": "error", 196 | "name": "Unauthorized", 197 | "inputs": [] 198 | } 199 | ] 200 | -------------------------------------------------------------------------------- /abi/pookyball/Pressure.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "constructor", 4 | "inputs": [ 5 | { 6 | "name": "_pok", 7 | "type": "address", 8 | "internalType": "contract IPOK" 9 | }, 10 | { 11 | "name": "_treasury", 12 | "type": "address", 13 | "internalType": "address" 14 | } 15 | ], 16 | "stateMutability": "nonpayable" 17 | }, 18 | { 19 | "type": "function", 20 | "name": "floors", 21 | "inputs": [ 22 | { 23 | "name": "", 24 | "type": "uint256", 25 | "internalType": "uint256" 26 | } 27 | ], 28 | "outputs": [ 29 | { 30 | "name": "", 31 | "type": "uint8", 32 | "internalType": "uint8" 33 | } 34 | ], 35 | "stateMutability": "view" 36 | }, 37 | { 38 | "type": "function", 39 | "name": "floorsNAT", 40 | "inputs": [ 41 | { 42 | "name": "", 43 | "type": "uint256", 44 | "internalType": "uint256" 45 | } 46 | ], 47 | "outputs": [ 48 | { 49 | "name": "", 50 | "type": "uint256", 51 | "internalType": "uint256" 52 | } 53 | ], 54 | "stateMutability": "view" 55 | }, 56 | { 57 | "type": "function", 58 | "name": "floorsPOK", 59 | "inputs": [ 60 | { 61 | "name": "", 62 | "type": "uint256", 63 | "internalType": "uint256" 64 | } 65 | ], 66 | "outputs": [ 67 | { 68 | "name": "", 69 | "type": "uint256", 70 | "internalType": "uint256" 71 | } 72 | ], 73 | "stateMutability": "view" 74 | }, 75 | { 76 | "type": "function", 77 | "name": "inflate", 78 | "inputs": [ 79 | { 80 | "name": "tokenId", 81 | "type": "uint256", 82 | "internalType": "uint256" 83 | }, 84 | { 85 | "name": "current", 86 | "type": "uint8", 87 | "internalType": "uint8" 88 | }, 89 | { 90 | "name": "amount", 91 | "type": "uint8", 92 | "internalType": "uint8" 93 | } 94 | ], 95 | "outputs": [], 96 | "stateMutability": "payable" 97 | }, 98 | { 99 | "type": "function", 100 | "name": "priceNAT", 101 | "inputs": [ 102 | { 103 | "name": "current", 104 | "type": "uint8", 105 | "internalType": "uint8" 106 | }, 107 | { 108 | "name": "amount", 109 | "type": "uint8", 110 | "internalType": "uint8" 111 | } 112 | ], 113 | "outputs": [ 114 | { 115 | "name": "", 116 | "type": "uint256", 117 | "internalType": "uint256" 118 | } 119 | ], 120 | "stateMutability": "view" 121 | }, 122 | { 123 | "type": "function", 124 | "name": "pricePOK", 125 | "inputs": [ 126 | { 127 | "name": "current", 128 | "type": "uint8", 129 | "internalType": "uint8" 130 | }, 131 | { 132 | "name": "amount", 133 | "type": "uint8", 134 | "internalType": "uint8" 135 | } 136 | ], 137 | "outputs": [ 138 | { 139 | "name": "", 140 | "type": "uint256", 141 | "internalType": "uint256" 142 | } 143 | ], 144 | "stateMutability": "view" 145 | }, 146 | { 147 | "type": "event", 148 | "name": "Inflated", 149 | "inputs": [ 150 | { 151 | "name": "tokenId", 152 | "type": "uint256", 153 | "indexed": true, 154 | "internalType": "uint256" 155 | }, 156 | { 157 | "name": "current", 158 | "type": "uint8", 159 | "indexed": false, 160 | "internalType": "uint8" 161 | }, 162 | { 163 | "name": "amount", 164 | "type": "uint8", 165 | "indexed": false, 166 | "internalType": "uint8" 167 | } 168 | ], 169 | "anonymous": false 170 | }, 171 | { 172 | "type": "error", 173 | "name": "InsufficientPOK", 174 | "inputs": [ 175 | { 176 | "name": "expected", 177 | "type": "uint256", 178 | "internalType": "uint256" 179 | }, 180 | { 181 | "name": "actual", 182 | "type": "uint256", 183 | "internalType": "uint256" 184 | } 185 | ] 186 | }, 187 | { 188 | "type": "error", 189 | "name": "InsufficientValue", 190 | "inputs": [ 191 | { 192 | "name": "expected", 193 | "type": "uint256", 194 | "internalType": "uint256" 195 | }, 196 | { 197 | "name": "actual", 198 | "type": "uint256", 199 | "internalType": "uint256" 200 | } 201 | ] 202 | }, 203 | { 204 | "type": "error", 205 | "name": "InvalidParameters", 206 | "inputs": [ 207 | { 208 | "name": "current", 209 | "type": "uint256", 210 | "internalType": "uint256" 211 | }, 212 | { 213 | "name": "amount", 214 | "type": "uint256", 215 | "internalType": "uint256" 216 | } 217 | ] 218 | }, 219 | { 220 | "type": "error", 221 | "name": "TransferFailed", 222 | "inputs": [ 223 | { 224 | "name": "recipient", 225 | "type": "address", 226 | "internalType": "address" 227 | }, 228 | { 229 | "name": "amount", 230 | "type": "uint256", 231 | "internalType": "uint256" 232 | } 233 | ] 234 | } 235 | ] 236 | -------------------------------------------------------------------------------- /abi/stickers/StickersManager.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "constructor", 4 | "inputs": [ 5 | { 6 | "name": "_controller", 7 | "type": "address", 8 | "internalType": "contract IStickersController" 9 | } 10 | ], 11 | "stateMutability": "nonpayable" 12 | }, 13 | { 14 | "type": "function", 15 | "name": "attach", 16 | "inputs": [ 17 | { 18 | "name": "stickerId", 19 | "type": "uint256", 20 | "internalType": "uint256" 21 | }, 22 | { 23 | "name": "pookyballId", 24 | "type": "uint256", 25 | "internalType": "uint256" 26 | } 27 | ], 28 | "outputs": [], 29 | "stateMutability": "nonpayable" 30 | }, 31 | { 32 | "type": "function", 33 | "name": "controller", 34 | "inputs": [], 35 | "outputs": [ 36 | { 37 | "name": "", 38 | "type": "address", 39 | "internalType": "contract IStickersController" 40 | } 41 | ], 42 | "stateMutability": "view" 43 | }, 44 | { 45 | "type": "function", 46 | "name": "pookyball", 47 | "inputs": [], 48 | "outputs": [ 49 | { 50 | "name": "", 51 | "type": "address", 52 | "internalType": "contract IPookyball" 53 | } 54 | ], 55 | "stateMutability": "view" 56 | }, 57 | { 58 | "type": "function", 59 | "name": "replace", 60 | "inputs": [ 61 | { 62 | "name": "stickerId", 63 | "type": "uint256", 64 | "internalType": "uint256" 65 | }, 66 | { 67 | "name": "previousStickerId", 68 | "type": "uint256", 69 | "internalType": "uint256" 70 | }, 71 | { 72 | "name": "pookyballId", 73 | "type": "uint256", 74 | "internalType": "uint256" 75 | } 76 | ], 77 | "outputs": [], 78 | "stateMutability": "nonpayable" 79 | }, 80 | { 81 | "type": "function", 82 | "name": "slots", 83 | "inputs": [ 84 | { 85 | "name": "pookyballId", 86 | "type": "uint256", 87 | "internalType": "uint256" 88 | } 89 | ], 90 | "outputs": [ 91 | { 92 | "name": "total", 93 | "type": "uint256", 94 | "internalType": "uint256" 95 | }, 96 | { 97 | "name": "unlocked", 98 | "type": "uint256", 99 | "internalType": "uint256" 100 | }, 101 | { 102 | "name": "free", 103 | "type": "uint256", 104 | "internalType": "uint256" 105 | } 106 | ], 107 | "stateMutability": "view" 108 | }, 109 | { 110 | "type": "function", 111 | "name": "stickers", 112 | "inputs": [], 113 | "outputs": [ 114 | { 115 | "name": "", 116 | "type": "address", 117 | "internalType": "contract IStickers" 118 | } 119 | ], 120 | "stateMutability": "view" 121 | }, 122 | { 123 | "type": "error", 124 | "name": "InsufficientFreeSlot", 125 | "inputs": [ 126 | { 127 | "name": "pookyballId", 128 | "type": "uint256", 129 | "internalType": "uint256" 130 | } 131 | ] 132 | }, 133 | { 134 | "type": "error", 135 | "name": "OwnershipRequired", 136 | "inputs": [ 137 | { 138 | "name": "token", 139 | "type": "address", 140 | "internalType": "address" 141 | }, 142 | { 143 | "name": "tokenId", 144 | "type": "uint256", 145 | "internalType": "uint256" 146 | } 147 | ] 148 | } 149 | ] 150 | -------------------------------------------------------------------------------- /bytecode/common/LevelUp.bin: -------------------------------------------------------------------------------- 1 | 0x 2 | -------------------------------------------------------------------------------- /bytecode/common/Signer.bin: -------------------------------------------------------------------------------- 1 | 0x 2 | -------------------------------------------------------------------------------- /bytecode/common/Treasury.bin: -------------------------------------------------------------------------------- 1 | 0x 2 | -------------------------------------------------------------------------------- /bytecode/pookyball/Pressure.bin: -------------------------------------------------------------------------------- 1 | 0x6101c0604052600a60c0908152601460e052601e61010052602861012052603261014052603c61016052604b6101805260646101a052610043906000906008610192565b50604080516101008101825266e35fa931a00000815266bf22d0a0ea8000602082015266a0952917d90000918101919091526686a5d964fcc0006060820152667154e18855c0006080820152665f36753ffb000060a0820152664fefa17b72400060c0820152664325732a41400060e08201526100c4906001906008610238565b506040805161010081018252671fb980e3648b00008152671aa535d3d0c00000602082015267166286f436b48000918101919091526712cded5226a780006060820152670fcafcf87a6500006080820152670d4464ef5546000060a0820152670b24d43edaa3800060c082015267095a876dd29d000060e082015261014d90600290600861027e565b5034801561015a57600080fd5b5060405162000b4a38038062000b4a83398101604081905261017b916102f1565b6001600160a01b039182166080521660a05261032b565b82805482825590600052602060002090601f016020900481019282156102285791602002820160005b838211156101f957835183826101000a81548160ff021916908360ff16021790555092602001926001016020816000010492830192600103026101bb565b80156102265782816101000a81549060ff02191690556001016020816000010492830192600103026101f9565b505b506102349291506102c4565b5090565b828054828255906000526020600020908101928215610228579160200282015b82811115610228578251829066ffffffffffffff16905591602001919060010190610258565b828054828255906000526020600020908101928215610228579160200282015b8281111561022857825182906001600160401b031690559160200191906001019061029e565b5b8082111561023457600081556001016102c5565b6001600160a01b03811681146102ee57600080fd5b50565b6000806040838503121561030457600080fd5b825161030f816102d9565b6020840151909250610320816102d9565b809150509250929050565b60805160a0516107eb6200035f60003960006102a101526000818161036f015281816103f501526104a801526107eb6000f3fe6080604052600436106100555760003560e01c806311a43c4c1461005a5780631dad3bde1461008d57806347f39471146100ad57806355705d6d146100df57806388c97820146100ff578063a6a3cfae1461011f575b600080fd5b34801561006657600080fd5b5061007a6100753660046106ac565b610134565b6040519081526020015b60405180910390f35b34801561009957600080fd5b5061007a6100a83660046106db565b610155565b3480156100b957600080fd5b506100cd6100c83660046106ac565b6101bb565b60405160ff9091168152602001610084565b3480156100eb57600080fd5b5061007a6100fa3660046106ac565b6101ef565b34801561010b57600080fd5b5061007a61011a3660046106db565b6101ff565b61013261012d36600461070e565b61025a565b005b6001818154811061014457600080fd5b600091825260209091200154905081565b60006101b2838360028054806020026020016040519081016040528092919081815260200182805480156101a857602002820191906000526020600020905b815481526020019060010190808311610194575b5050505050610552565b90505b92915050565b600081815481106101cb57600080fd5b9060005260206000209060209182820401919006915054906101000a900460ff1681565b6002818154811061014457600080fd5b60006101b2838360018054806020026020016040519081016040528092919081815260200182805480156101a85760200282019190600052602060002090815481526020019060010190808311610194575050505050610552565b341561034157600061026c83836101ff565b90508034101561029d57604051631c102d6360e21b8152346004820152602481018290526044015b60405180910390fd5b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168260405160006040518083038185875af1925050503d806000811461030a576040519150601f19603f3d011682016040523d82523d6000602084013e61030f565b606091505b505090508061033a57604051630e21dcbb60e11b815233600482015260248101839052604401610294565b505061050e565b600061034d8383610155565b6040516370a0823160e01b815233600482015290915081906001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016906370a0823190602401602060405180830381865afa1580156103b6573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103da919061074a565b101561048c576040516370a0823160e01b81523360048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906370a0823190602401602060405180830381865afa158015610444573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610468919061074a565b604051636c84984560e11b8152600481019190915260248101829052604401610294565b604051632770a7eb60e21b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690639dc29fac90604401600060405180830381600087803b1580156104f457600080fd5b505af1158015610508573d6000803e3d6000fd5b50505050505b6040805160ff80851682528316602082015284917fae8e536e52847e375aeccd7c0fe477cd7394d10e10101278e98d83ad0633b3da910160405180910390a2505050565b600060646105608486610779565b60ff16111561058f57604051630135ce2d60e31b815260ff808616600483015284166024820152604401610294565b6000805b6000548110156106a35760ff8516156106a357600081815481106105b9576105b9610792565b90600052602060002090602091828204019190069054906101000a900460ff1660ff168660ff161161069b57600086600083815481106105fb576105fb610792565b90600052602060002090602091828204019190069054906101000a900460ff1660016106279190610779565b61063191906107a8565b905060008660ff168260ff1611610648578161064a565b865b90508060ff1686848151811061066257610662610792565b602002602001015161067491906107c1565b61067e90856107d8565b935061068a8189610779565b975061069681886107a8565b965050505b600101610593565b50949350505050565b6000602082840312156106be57600080fd5b5035919050565b803560ff811681146106d657600080fd5b919050565b600080604083850312156106ee57600080fd5b6106f7836106c5565b9150610705602084016106c5565b90509250929050565b60008060006060848603121561072357600080fd5b83359250610733602085016106c5565b9150610741604085016106c5565b90509250925092565b60006020828403121561075c57600080fd5b5051919050565b634e487b7160e01b600052601160045260246000fd5b60ff81811683821601908111156101b5576101b5610763565b634e487b7160e01b600052603260045260246000fd5b60ff82811682821603908111156101b5576101b5610763565b80820281158282048414176101b5576101b5610763565b808201808211156101b5576101b561076356 2 | -------------------------------------------------------------------------------- /bytecode/stickers/StickersManager.bin: -------------------------------------------------------------------------------- 1 | 0x60e060405234801561001057600080fd5b50604051610c70380380610c7083398101604081905261002f91610142565b6001600160a01b03811660c0819052604080516308717a6760e31b8152905163438bd338916004808201926020929091908290030181865afa158015610079573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061009d9190610142565b6001600160a01b03166080816001600160a01b031681525050806001600160a01b031663109876836040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100f4573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101189190610142565b6001600160a01b031660a05250610166565b6001600160a01b038116811461013f57600080fd5b50565b60006020828403121561015457600080fd5b815161015f8161012a565b9392505050565b60805160a05160c051610a896101e76000396000818161012d015281816102d20152818161055901526107e1015260008181606c015281816101570152818161047101528181610501015281816106cd015261075d01526000818160de0152818161038801528181610418015281816105e901526106790152610a896000f3fe608060405234801561001057600080fd5b50600436106100625760003560e01c80631098768314610067578063387dd9e9146100ab578063438bd338146100d95780635e523a2714610100578063c55e0ec514610115578063f77c479114610128575b600080fd5b61008e7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020015b60405180910390f35b6100be6100b9366004610818565b61014f565b604080519384526020840192909252908201526060016100a2565b61008e7f000000000000000000000000000000000000000000000000000000000000000081565b61011361010e366004610831565b610364565b005b61011361012336600461085d565b6105c5565b61008e7f000000000000000000000000000000000000000000000000000000000000000081565b6000806000807f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663e3684e39866040518263ffffffff1660e01b81526004016101a391815260200190565b608060405180830381865afa1580156101c0573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101e491906108c6565b90506000815160048111156101fb576101fb610939565b036102095760049350610291565b60018151600481111561021e5761021e610939565b0361022c5760069350610291565b60028151600481111561024157610241610939565b0361024f5760089350610291565b60038151600481111561026457610264610939565b0361027257600a9350610291565b60048151600481111561028757610287610939565b0361029157600c93505b600a816020015160056102a49190610965565b6102ae919061097e565b60405163387dd9e960e01b8152600481018790529093506000906001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063387dd9e990602401600060405180830381865afa158015610319573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f1916820160405261034191908101906109a0565b51905083811115610350578093505b61035a8185610a46565b9496939550505050565b6040516331a9108f60e11b8152600481018490528390829033906001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001690636352211e90602401602060405180830381865afa1580156103cf573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103f39190610a59565b6001600160a01b0316146104515760405163886680d960e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000166004820152602481018390526044015b60405180910390fd5b6040516331a9108f60e11b81526004810182905233906001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001690636352211e90602401602060405180830381865afa1580156104b8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104dc9190610a59565b6001600160a01b0316146105355760405163886680d960e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016600482015260248101829052604401610448565b604051635e523a2760e01b81526004810186905260248101859052604481018490527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690635e523a27906064015b600060405180830381600087803b1580156105a657600080fd5b505af11580156105ba573d6000803e3d6000fd5b505050505050505050565b6040516331a9108f60e11b8152600481018390528290829033906001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001690636352211e90602401602060405180830381865afa158015610630573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106549190610a59565b6001600160a01b0316146106ad5760405163886680d960e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016600482015260248101839052604401610448565b6040516331a9108f60e11b81526004810182905233906001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001690636352211e90602401602060405180830381865afa158015610714573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107389190610a59565b6001600160a01b0316146107915760405163886680d960e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016600482015260248101829052604401610448565b600061079c8461014f565b92505050806000036107c457604051636aebd16b60e01b815260048101859052602401610448565b60405163c55e0ec560e01b815260048101869052602481018590527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063c55e0ec59060440161058c565b60006020828403121561082a57600080fd5b5035919050565b60008060006060848603121561084657600080fd5b505081359360208301359350604090920135919050565b6000806040838503121561087057600080fd5b50508035926020909101359150565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff811182821017156108be576108be61087f565b604052919050565b6000608082840312156108d857600080fd5b6040516080810181811067ffffffffffffffff821117156108fb576108fb61087f565b60405282516005811061090d57600080fd5b808252506020830151602082015260408301516040820152606083015160608201528091505092915050565b634e487b7160e01b600052602160045260246000fd5b634e487b7160e01b600052601160045260246000fd5b808201808211156109785761097861094f565b92915050565b60008261099b57634e487b7160e01b600052601260045260246000fd5b500490565b600060208083850312156109b357600080fd5b825167ffffffffffffffff808211156109cb57600080fd5b818501915085601f8301126109df57600080fd5b8151818111156109f1576109f161087f565b8060051b9150610a02848301610895565b8181529183018401918481019088841115610a1c57600080fd5b938501935b83851015610a3a57845182529385019390850190610a21565b98975050505050505050565b818103818111156109785761097861094f565b600060208284031215610a6b57600080fd5b81516001600160a01b0381168114610a8257600080fd5b939250505056 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: false 4 | patch: false 5 | 6 | ignore: 7 | - './script/' 8 | - './test/' 9 | -------------------------------------------------------------------------------- /docs/alchemy_webhooks/energy.md: -------------------------------------------------------------------------------- 1 | # Energy Alchemy Webhook 2 | 3 | ## URL 4 | 5 | https://blockchain-events-service-t57fod3riq-oa.a.run.app/api/webhook/energy 6 | 7 | ## Graphql Query 8 | 9 | ``` 10 | { 11 | block(hash: "0xcd823713b39008f1f6e909103157de6b78d51e22c923bfcd1cab2fa8b5df3092") { 12 | hash 13 | number 14 | timestamp 15 | logs( 16 | filter: { 17 | addresses: [ 18 | # Energy 19 | "0xcE3bAF0c9989beC8e39B0D063Ffdf5C09373a510" 20 | ] 21 | topics: [[ 22 | # Transfer(address,address,uint256) 23 | "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" 24 | ]] 25 | } 26 | ) { 27 | topics 28 | data 29 | index 30 | account { 31 | address 32 | } 33 | transaction { 34 | hash 35 | from { 36 | address 37 | } 38 | value 39 | status 40 | } 41 | } 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/alchemy_webhooks/pookyball.md: -------------------------------------------------------------------------------- 1 | # Pookyball Alchemy Webhook 2 | 3 | ## URL 4 | 5 | https://blockchain-events-service-t57fod3riq-oa.a.run.app/api/webhook/pookyball 6 | 7 | ## Graphql Query 8 | 9 | ``` 10 | { 11 | block(hash: "0x6361d2726ffc5b7ab49a006782513ce2358ab06e4bfd35c9ca1080fa86b2fc00") { 12 | hash 13 | number 14 | timestamp 15 | logs( 16 | filter: { 17 | addresses: [ 18 | # Pookyball 19 | "0x91290239CD991eB8B748FaFe3fb2401C5ec3c988" 20 | ] 21 | topics: [[ 22 | # Seedset(uint256,uint256) 23 | "0x14296754697e325872a9c14eb682f467bc46b15a78ae9420d7a13a7cd3833b2c" 24 | # Transfer(uint256,address,address) 25 | "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" 26 | # LevelChanged(uint256,uint256) 27 | "0x8bdaee675270281b7bc2d5b9ced20517ecf5ce96158973ef78072a7bc1491b44" 28 | # PXPChanged(uint256,uint256) 29 | "0x334246ed50b046a9933ddd38bbb387c51e3709753687788af1c5207d3689761b" 30 | ]] 31 | } 32 | ) { 33 | topics 34 | data 35 | index 36 | account { 37 | address 38 | } 39 | transaction { 40 | hash 41 | from { 42 | address 43 | } 44 | value 45 | status 46 | } 47 | } 48 | } 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/alchemy_webhooks/pookyball_ascension.md: -------------------------------------------------------------------------------- 1 | # Pookyball Ascension Alchemy Webhook 2 | 3 | ## URL 4 | 5 | https://blockchain-events-service-t57fod3riq-oa.a.run.app/api/webhook/pookyball/ascension 6 | 7 | ## Graphql Query 8 | 9 | ``` 10 | { 11 | block(hash: "0xc0d32f45c8db019a877f8cfe4b021f3adcdb4d9d90dc2942f716155b4e9bbe9d") { 12 | hash 13 | number 14 | timestamp 15 | logs( 16 | filter: { 17 | addresses: [ 18 | # Ascension 19 | "0x1369B4e6B4B1A8f1a6C3c79dCB5DbB251EdC0D30" 20 | ] 21 | topics: [[ 22 | # Ascended(uint256,uint8,uint256,uint256,string) 23 | "0x82c463f27adb1a5c8e7d0209eb97c6dcec687c389dfc71b7c0a6216a128571db" 24 | ]] 25 | } 26 | ) { 27 | topics 28 | data 29 | index 30 | account { 31 | address 32 | } 33 | transaction { 34 | hash 35 | from { 36 | address 37 | } 38 | value 39 | status 40 | } 41 | } 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/alchemy_webhooks/pookyball_pressure.md: -------------------------------------------------------------------------------- 1 | # Pookyball Pressure Alchemy Webhook 2 | 3 | ## URL 4 | 5 | https://blockchain-events-service-t57fod3riq-oa.a.run.app/api/webhook/pookyball/pressure 6 | 7 | ## Graphql Query 8 | 9 | ``` 10 | { 11 | block(hash: "0xae3c3dabd292cbb43c40afc615d5563a1dfa5f1a92d801a239f361570a519ca5") { 12 | hash 13 | number 14 | timestamp 15 | logs( 16 | filter: { 17 | addresses: [ 18 | # Pressure 19 | "0x3876E098488092af30aFACBF4e99553372CaAedD" 20 | ] 21 | topics: [[ 22 | # Inflated(uint256,uint8,uint8) 23 | "0xae8e536e52847e375aeccd7c0fe477cd7394d10e10101278e98d83ad0633b3da" 24 | ]] 25 | } 26 | ) { 27 | topics 28 | data 29 | index 30 | account { 31 | address 32 | } 33 | transaction { 34 | hash 35 | from { 36 | address 37 | } 38 | value 39 | status 40 | } 41 | } 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/alchemy_webhooks/pookyball_reroll.md: -------------------------------------------------------------------------------- 1 | # Pookyball Reroll Alchemy Webhook 2 | 3 | ## URL 4 | 5 | https://blockchain-events-service-t57fod3riq-oa.a.run.app/api/webhook/pookyball/reroll 6 | 7 | ## Graphql Query 8 | 9 | ``` 10 | { 11 | block(hash: "0xa9cb852339ea8ac81780d8fd07ab893f4abf88531b182270b232d9062c4bebfe") { 12 | hash 13 | number 14 | timestamp 15 | logs( 16 | filter: { 17 | addresses: [ 18 | # Reroll 19 | "0x5c2E6a21813751411c4d445F9DFa2638c8271f61" 20 | ] 21 | topics: [[ 22 | # Reroll(uint256,uint256) 23 | "0xd49929b567bcaca6fae0329912961a4872c232438a2d478bb94c0775ff1e1f62" 24 | ]] 25 | } 26 | ) { 27 | topics 28 | data 29 | index 30 | account { 31 | address 32 | } 33 | transaction { 34 | hash 35 | from { 36 | address 37 | } 38 | value 39 | status 40 | } 41 | } 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/alchemy_webhooks/rewards.md: -------------------------------------------------------------------------------- 1 | # Rewards Alchemy Webhook 2 | 3 | ## URL 4 | 5 | https://blockchain-events-service-t57fod3riq-oa.a.run.app/api/webhook/reward 6 | 7 | ## Graphql Query 8 | 9 | ``` 10 | { 11 | block(hash: "0x53484e705a10026632c689a082b0723b50c79150acbf634656698a24d2c8c0a7") { 12 | hash 13 | number 14 | timestamp 15 | logs( 16 | filter: { 17 | addresses: [ 18 | # Reward 19 | "0xBa609c15697617f9a243A5E0A609371044497719" 20 | ] 21 | topics: [[ 22 | # RewardsClaimed(address,(uint256,uint256,(uint256,uint256)[],uint8[],bytes32[]),string) 23 | "0x333a6859dbd4f68eb6492cd6f8ad2b4ae4ae870747adebb44935fb4a2603cedf" 24 | ]] 25 | } 26 | ) { 27 | topics 28 | data 29 | index 30 | account { 31 | address 32 | } 33 | transaction { 34 | hash 35 | from { 36 | address 37 | } 38 | value 39 | status 40 | } 41 | } 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/alchemy_webhooks/stickers.md: -------------------------------------------------------------------------------- 1 | # Stickers Alchemy Webhook 2 | 3 | ## URL 4 | 5 | https://blockchain-events-service-t57fod3riq-oa.a.run.app/api/webhook/sticker 6 | 7 | ## Graphql Query 8 | 9 | ``` 10 | { 11 | block(hash: "0x7a71beba18bbcf6c9a057964d613c66c7fd7579a0ebae931b7ce66f80836e478") { 12 | hash 13 | number 14 | timestamp 15 | logs( 16 | filter: { 17 | addresses: [ 18 | # Sticker 19 | "0x975d59D5ff4c1D4ED908892377E23Bf00c40f7aD" 20 | ] 21 | topics: [[ 22 | # SeedSet(uint256,uint256) 23 | "0x14296754697e325872a9c14eb682f467bc46b15a78ae9420d7a13a7cd3833b2c" 24 | # Transfer(uint256,address,address) 25 | "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" 26 | # LevelChanged(uint256,uint256) 27 | "0x8bdaee675270281b7bc2d5b9ced20517ecf5ce96158973ef78072a7bc1491b44" 28 | ]] 29 | } 30 | ) { 31 | topics 32 | data 33 | index 34 | account { 35 | address 36 | } 37 | transaction { 38 | hash 39 | from { 40 | address 41 | } 42 | value 43 | status 44 | } 45 | } 46 | } 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/alchemy_webhooks/stickers_ascension.md: -------------------------------------------------------------------------------- 1 | # Stickers Ascension Alchemy Webhook 2 | 3 | ## URL 4 | 5 | https://blockchain-events-service-t57fod3riq-oa.a.run.app/api/webhook/sticker/ascension 6 | 7 | ## Graphql Query 8 | 9 | ``` 10 | { 11 | block(hash: "0xdf758a3c5698d68094e113f25a9ca1a2f7a8d102e9351dea92afbeffc36b7e94") { 12 | hash 13 | number 14 | timestamp 15 | logs( 16 | filter: { 17 | addresses: [ 18 | # Ascension Stickers 19 | "0xCb20FEe5113F10fE06BD9452d2E5cD1972161006" 20 | ] 21 | topics: [[ 22 | # Ascended(uint256 indexed tokenId, uint8 rarity, uint256[] parts, string data) 23 | "0x11869a71baced1026064951c3a99898feaafa2a01af499fe82eb2396891db7ef" 24 | ]] 25 | } 26 | ) { 27 | topics 28 | data 29 | index 30 | account { 31 | address 32 | } 33 | transaction { 34 | hash 35 | from { 36 | address 37 | } 38 | value 39 | status 40 | } 41 | } 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/alchemy_webhooks/stickers_controller.md: -------------------------------------------------------------------------------- 1 | # Stickers Controller Alchemy Webhook 2 | 3 | ## URL 4 | 5 | https://blockchain-events-service-t57fod3riq-oa.a.run.app/api/webhook/sticker/controller 6 | 7 | ## Graphql Query 8 | 9 | ``` 10 | { 11 | block(hash: "0x40bb2ace6b4d4301747320b9107183def831554b018e0a2a4cdde7199bb8bc6f") { 12 | hash 13 | number 14 | timestamp 15 | logs( 16 | filter: { 17 | addresses: [ 18 | # StickerController 19 | "0xB9b85d3DCF4bB8c064D9F594354F07F5DCE12daF" 20 | ] 21 | topics: [[ 22 | # StickerAttached(uint256,uint256) 23 | "0xa0caedfb09abf5bb3acfa131de435a0b7aa4884a8b70d261e35fd563f843e1ac" 24 | # StickerReplaced(uint256,uint256,uint256) 25 | "0xf22098f7acee83ee20f4856c625c4321d76cb426bcb4e2ae67d7b3eccf00bd85" 26 | # StickerDetached(uint256,uint256) 27 | "0x47d2a53d2ac8e763206badd9286235cacd85eb4d108ee384e77c6fedbc4f0f02" 28 | ]] 29 | } 30 | ) { 31 | topics 32 | data 33 | index 34 | account { 35 | address 36 | } 37 | transaction { 38 | hash 39 | from { 40 | address 41 | } 42 | value 43 | status 44 | } 45 | } 46 | } 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/chains.md: -------------------------------------------------------------------------------- 1 | # Chains on which contracts are deployed 2 | 3 | ## Chain info: 4 | 5 | - Network Name: `Polygon Mainnet` 6 | - New RPC URL: [`https://polygon-rpc.com`](https://polygon-rpc.com) 7 | - Chain ID: `137` 8 | - Currency Symbol: `MATIC` 9 | - Block Explorer URL: [`https://polygonscan.com`](https://polygonscan.com) 10 | - Faucet: [`https://faucet.polygon.technology/`](https://faucet.polygon.technology/) 11 | 12 | ## Chain info: 13 | 14 | - Network Name: `Polygon Amoy Testnet` 15 | - New RPC URL: [`https://rpc-amoy.polygon.technology/`](https://rpc-amoy.polygon.technology/) 16 | - Chain ID: `80002` 17 | - Currency Symbol: `MATIC` 18 | - Block Explorer URL: [`https://www.oklink.com/amoy`](https://www.oklink.com/amoy) 19 | - Faucet: [`https://faucet.polygon.technology/`](https://faucet.polygon.technology/) 20 | -------------------------------------------------------------------------------- /docs/codebase.md: -------------------------------------------------------------------------------- 1 | # Codebase 2 | 3 | ## General rules 4 | 5 | ### NatSpec 6 | 7 | NatSpec MUST be written using the triple slash `///` syntax. 8 | 9 | Contract header of the source code must at least include: 10 | 11 | ```solidity 12 | /// @title MyContract 13 | /// @author Author1, Author2 for Pooky Labs Ltd. 14 | ``` 15 | 16 | If contract includes `@notice` or `@dev` section, a blank line MUST be inserted after the `@title` and `@author` tags. 17 | 18 | ```solidity 19 | /// @title MyContract 20 | /// @author Author1, Author2 for Pooky Labs Ltd. 21 | /// 22 | /// @notice Hello world 23 | /// @dev This is me 24 | ``` 25 | 26 | ### Imports 27 | 28 | Imports MUST use the explicit bracket syntax: 29 | 30 | ```solidity 31 | // Good 32 | import { ERC721A } from "ERC721A/ERC721.sol": 33 | 34 | // Bad 35 | import "ERC721A/ERC721.sol": 36 | ``` 37 | 38 | Imports MUST be sorted with the following rules: 39 | 40 | 1. First group: vendor dependencies 41 | 2. Source code (imports starting with `@/`) 42 | 3. Test code (imports starting with `@test/`) 43 | 4. Script code (imports starting with `@script/`) 44 | 45 | ## Contract updates 46 | 47 | Contracts that have already been deployed to mainnet **MUST NOT be updated**. 48 | 49 | A few exceptions to this rules: 50 | 51 | - Comments and/or documentation update 52 | - File/directory architecture changes 53 | - Imports path updates 54 | - Syntactic and/or grammar typos 55 | -------------------------------------------------------------------------------- /docs/stickers.md: -------------------------------------------------------------------------------- 1 | # Stickers 2 | 3 | Stickers are the second type of NFTs available on Pooky. 4 | 5 | Stickers are add-ons for Pookyballs that increase their in-game statistics. 6 | On-chain metadata is limited to the Sticker rarity, level and random seed. 7 | Please refer to the [Pooky whitepaper](https://whitepaper.pooky.gg/stickers) for more details on the Stickers. 8 | 9 | ## Contracts list 10 | 11 | ### [`Stickers.sol`](../src/stickers/Stickers.sol) 12 | 13 | Stickers smart contract follows the ERC-721 specification and take advantage of the [ERC721A implementation](https://github.com/chiru-labs/ERC721A#readme). 14 | ERC721A allows to save hug amounts of gas when minting multiple stickers at the same time. 15 | 16 | The Stickers contract is based on our [`BaseERC721A.sol`](../src/common/BaseERC721A.sol) contract that implements: 17 | 18 | - ERC721 NFT standard 19 | - ERC2891 royalties standard 20 | - OpenSea registry to enforce the on-chain royalties 21 | 22 | Minting stickers is only possible via the `mint` function. 23 | This function will automatically call the Chainlink VRF cooridinator and request the seeds for the newly minted Stickers. 24 | 25 | ### [`StickersController.sol`](../src/stickers/StickersController.sol) 26 | 27 | The `StickersController` is in charge of the Pookyballs <=> Stickers association. 28 | It is not supposed to run any check if a Sticker can be attached to a Pookyball or not. 29 | `StickersController` contract is designed to be future proof and should be considered a a long-lived, stable implementation. 30 | 31 | `StickersController` is capable of answering the following questions: 32 | 33 | 1. `StickersController.attachedTo`: Given a Sticker ID, what is it parent Pookyball id? Zero means that the Sticker is not attached to a Pookyball. 34 | 2. `StickersController.slots`: Given a Pookyball ID, what are the Stickers IDs attached to this particular Pookyball? 35 | 36 | `StickersController` also exposes 3 methods, that are not callable unless the sender has the right permission. 37 | 38 | | Method | Description | Role required | Event | 39 | | --------- | ------------------------------------------------------------ | ------------- | ----------------- | 40 | | `attach` | Attach a Sticker to a Pookyball | `LINKER` | `StickerAttached` | 41 | | `replace` | Replace a Sticker of a Pookyball (burn the previous Sticker) | `REPLACER` | `StickerReplaced` | 42 | | `detach` | Detach a Sticker from a Pookyabll | `REMOVER` | `StickerDetached` | 43 | 44 | To ensure that: 45 | 46 | - a Sticker cannot be attached twice to different Pookyballs 47 | - a Sticker cannot be sold after being attached to a Pookyball 48 | - transfering a Pookyball also transfers the Stickers attached to the Pookyball 49 | 50 | Attaching a Sticker to a Pookyball transfers the Stickers from the sender inventory to the `StickersController` itself. 51 | 52 | ### [`StickersManager.sol`](../src/stickers/StickersManager.sol) 53 | 54 | `StickersManager` can be seen at the Stickers game implementation that uses the `StickersController` primitives. 55 | It is likely to be replaced in the future by a new contract that will implement new rules. 56 | 57 | Currently, the `StickersManager`: 58 | 59 | - controls how much slots are available on Pookyballs depending on their level and rarities (`StickersManager.slots`) 60 | - ensure that a sticker can be attach to a Pookyball only if the Pookyball has a free slot slot 61 | 62 | ### [`StickersLevelUp.sol`](../src/stickers/StickersLevelUp.sol) 63 | 64 | Stickers can be leveled up using the `StickersLevelUp` contract. 65 | `StickersLevelUp` is based on our [`LevelUpContract`](../src/common/LevelUp.sol). 66 | 67 | Since the we don't store the Stickers PXP on chain to save gas, users will have to query the back-end to get a signature that certify the PXP amount they pass to the `levelUp` function. 68 | 69 | ```solidity 70 | function levelUp(uint256 tokenId, uint256 increase, uint256 currentPXP, bytes calldata proof) external payable; 71 | ``` 72 | 73 | Applications may listen to the `LevelChanged` event of the `Stickers` contract to follow the changes of the Stickers levels. 74 | 75 | ### [`StickersSale.sol`](../src/stickers/StickersSale.sol) 76 | 77 | The Stickers can be minted in batch via the `StickersSale` contract. 78 | -------------------------------------------------------------------------------- /docs/wallet.md: -------------------------------------------------------------------------------- 1 | # Production environment 2 | 3 | The administrative privilege of granting/revoking roles is handled by a multi-signature wallet owned by multiple engineers of Pooky Labs. 4 | 5 | - polygon scan address [`0x3CC4F4372F83ad3C577eD6e1Aae3D244A1b955D5`](https://polygonscan.com/address/0x3CC4F4372F83ad3C577eD6e1Aae3D244A1b955D5). 6 | 7 | - safe wallet URL is [`https://app.safe.global/home?safe=matic:0x3CC4F4372F83ad3C577eD6e1Aae3D244A1b955D5`](https://app.safe.global/home?safe=matic:0x3CC4F4372F83ad3C577eD6e1Aae3D244A1b955D5) 8 | 9 | The Pooky dApp [pooky.gg](https://pooky.gg/app) serves as reference to the "official" Pooky smart contracts 10 | Pooky Labs may switch to other smart contract at any time on the app. 11 | In this scenario, Pooky Labs commits to airdrop the tokens to their owners in the new smart contracts. 12 | 13 | # Wallets 14 | 15 | - deployer - is the address from which all smart contracts are deployed 16 | - admin - is the owner of each smart contract 17 | - backend - is another wallet that is used to sign backend payloads 18 | - treasury - is the wallet where the funds are stored on each network 19 | 20 | ## Wallets Polygon mainnet network 21 | 22 | - Deployer - [`0xC0DE54132cA2B4BAce7C02AF31Aa9d3c1905f379`](https://polygonscan.com/address/0xC0DE54132cA2B4BAce7C02AF31Aa9d3c1905f379). 23 | - Admin - [`0xF00Db2f08D1F6b3f6089573085B5826Bb358e319`](https://polygonscan.com/address/0xF00Db2f08D1F6b3f6089573085B5826Bb358e319). 24 | - Backend - [`0xCAFE3e690bf74Ec274210E1c448130c1f8228513`](https://polygonscan.com/address/0xCAFE3e690bf74Ec274210E1c448130c1f8228513). 25 | - Treasury - [`0xBABA035d2e22073C3a2AadA404dae4f6A9D57BD7`](https://polygonscan.com/address/0xBABA035d2e22073C3a2AadA404dae4f6A9D57BD7). 26 | 27 | ## Wallets Polygon mainnet network 28 | 29 | - Deployer - [`0xC0DE54132cA2B4BAce7C02AF31Aa9d3c1905f379`](https://www.oklink.com/amoy/address/0xC0DE54132cA2B4BAce7C02AF31Aa9d3c1905f379). 30 | - Admin - [`0xF00Db2f08D1F6b3f6089573085B5826Bb358e319`](https://www.oklink.com/amoy/address/0xF00Db2f08D1F6b3f6089573085B5826Bb358e319). 31 | - Backend - [`0xCAFE3e690bf74Ec274210E1c448130c1f8228513`](https://www.oklink.com/amoy/address/0xCAFE3e690bf74Ec274210E1c448130c1f8228513). 32 | - Treasury - [`0xBABA035d2e22073C3a2AadA404dae4f6A9D57BD7`](https://www.oklink.com/amoy/address/0xBABA035d2e22073C3a2AadA404dae4f6A9D57BD7). 33 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | # Foundry config reference: https://github.com/foundry-rs/foundry/tree/master/config 2 | [profile.default] 3 | bytecode_hash = "none" 4 | cbor_metadata = false 5 | evm_version = "paris" 6 | fs_permissions = [{ access = "read", path = "./test/datasets" }] 7 | libs = ["lib"] 8 | optimizer = true 9 | optimizer_runs = 200 10 | out = "artifacts" 11 | solc = "0.8.22" 12 | src = "src" 13 | remappings = [ 14 | 'ERC721A/=lib/ERC721A/contracts/', 15 | 'chainlink/=lib/chainlink/contracts/src/v0.8/', 16 | 'ds-test/=lib/forge-std/lib/ds-test/src/', 17 | 'erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/', 18 | 'forge-std/=lib/forge-std/src/', 19 | 'openzeppelin-contracts/=lib/openzeppelin-contracts/', 20 | 'openzeppelin/=lib/openzeppelin-contracts/contracts/', 21 | 'operator-filter-registry/=lib/operator-filter-registry/src/', 22 | 'solady/=lib/solady/src/', 23 | '@/=src/', 24 | '@test/=test/', 25 | '@script/=script/', 26 | ] 27 | 28 | [profile.default.fuzz] 29 | runs = 256 30 | 31 | [profile.intense.fuzz] 32 | runs = 5_000 33 | 34 | [rpc_endpoints] 35 | amoy = "${AMOY_RPC_URL}" 36 | polygon = "${POLYGON_RPC_URL}" 37 | 38 | [etherscan] 39 | amoy = { key = "${POLYGONSCAN_API_KEY}" } 40 | polygon = { key = "${POLYGONSCAN_API_KEY}" } 41 | 42 | [fmt] 43 | bracket_spacing = true 44 | line_length = 100 45 | tab_width = 2 46 | 47 | [profile.ci] 48 | verbosity = 4 49 | 50 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | colors: false 2 | no_tty: true 3 | 4 | pre-commit: 5 | parallel: true 6 | commands: 7 | test: 8 | run: bash -c "NO_COLOR=1 forge test" 9 | artifacts: 10 | run: | 11 | pnpm rimraf abi bytecode 12 | pnpm artifacts 13 | git add ./abi ./bytecode 14 | lint: 15 | run: pnpm lint 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smart-contracts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "artifacts": "tsx script/artifacts.ts", 8 | "test": "forge test", 9 | "coverage": "forge coverage --report lcov --report summary", 10 | "lint": "solhint '{src,test}/**/*.sol'", 11 | "node": "anvil" 12 | }, 13 | "keywords": [], 14 | "maintainers": [ 15 | "Claudiu Micu " 16 | ], 17 | "contributors": [ 18 | "Mathieu Bour ", 19 | "Dusan Zdravkovic " 20 | ], 21 | "license": "MIT", 22 | "type": "module", 23 | "devDependencies": { 24 | "fast-glob": "^3.2.12", 25 | "lefthook": "^1.4.1", 26 | "rimraf": "^5.0.1", 27 | "solhint": "^3.4.1", 28 | "tsx": "^3.14.0", 29 | "typescript": "^5.0.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ERC721A/=lib/ERC721A/contracts/ 2 | chainlink/=lib/chainlink/contracts/src/v0.8/ 3 | ds-test/=lib/forge-std/lib/ds-test/src/ 4 | erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ 5 | forge-std/=lib/forge-std/src/ 6 | openzeppelin-contracts/=lib/openzeppelin-contracts/ 7 | openzeppelin/=lib/openzeppelin-contracts/contracts/ 8 | operator-filter-registry/=lib/operator-filter-registry/src/ 9 | solady/=lib/solady/src/ 10 | @/=src/ 11 | @test/=test/ 12 | @script/=script/ 13 | -------------------------------------------------------------------------------- /script/amoy/01_POK.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.21; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { POK } from "@/tokens/POK.sol"; 7 | 8 | contract DeployPOK is Script { 9 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 10 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 11 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 12 | 13 | function run() public { 14 | // ================ Deploy POK ================ 15 | vm.startBroadcast(deployerPK); 16 | 17 | POK deployed = new POK{ salt: salt }(adminAddress); 18 | 19 | console.log("POK deployed at:", address(deployed)); 20 | 21 | vm.stopBroadcast(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /script/amoy/02_Energy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.21; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { Energy } from "@/common/Energy.sol"; 7 | 8 | contract DeployEnergy is Script { 9 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 10 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 11 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 12 | address operatorAddress = vm.envAddress("OPERATOR_ADDRESS"); 13 | address treasuryAddress = vm.envAddress("TREASURY_ADDRESS"); 14 | 15 | function run() public { 16 | // ================ Deploy Energy ================ 17 | vm.startBroadcast(deployerPK); 18 | 19 | address[] memory operators = new address[](1); 20 | operators[0] = operatorAddress; 21 | 22 | Energy deployed = new Energy{ salt: salt }(adminAddress, operators, treasuryAddress, 5.88 ether); 23 | 24 | console.log("Energy deployed at:", address(deployed)); 25 | 26 | vm.stopBroadcast(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /script/amoy/03_NonceRegistry.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.21; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { NonceRegistry } from "@/common/NonceRegistry.sol"; 7 | 8 | contract DeployNonceRegistry is Script { 9 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 10 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 11 | address deployerAddress = vm.envAddress("DEPLOYER_ADDRESS"); 12 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 13 | address treasuryAddress = vm.envAddress("TREASURY_ADDRESS"); 14 | 15 | function run() public { 16 | // ================ Admin grant roles ================ 17 | vm.startBroadcast(deployerPK); 18 | 19 | address[] memory admins = new address[](2); 20 | admins[0] = deployerAddress; 21 | admins[1] = adminAddress; 22 | 23 | address[] memory operators = new address[](1); 24 | operators[0] = deployerAddress; 25 | 26 | NonceRegistry deployed = new NonceRegistry{ salt: salt }(admins, operators); 27 | 28 | console.log("NonceRegistry deployed at:", address(deployed)); 29 | 30 | vm.stopBroadcast(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /script/amoy/04_BoostPXP.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.21; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { BoostPXP } from "@/common/BoostPXP.sol"; 7 | import { NonceRegistry } from "@/common/NonceRegistry.sol"; 8 | 9 | contract DeployBoostPXP is Script { 10 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 11 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 12 | uint256 adminPK = vm.envUint("ADMIN_PK"); 13 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 14 | address signerAddress = vm.envAddress("BACKEND_ADDRESS"); 15 | address treasuryAddress = vm.envAddress("TREASURY_ADDRESS"); 16 | 17 | NonceRegistry nonces = NonceRegistry(vm.envAddress("CONTRACT_NONCE_REGISTRY_ADDRESS")); 18 | 19 | function run() public { 20 | // ================ Admin grant roles ================ 21 | vm.startBroadcast(deployerPK); 22 | 23 | BoostPXP boostPXP = 24 | new BoostPXP{ salt: salt }(nonces, adminAddress, signerAddress, treasuryAddress); 25 | 26 | console.log("BoostPXP deployed at:", address(boostPXP)); 27 | 28 | vm.stopBroadcast(); 29 | 30 | // ================ Admin grant roles ================ 31 | vm.startBroadcast(adminPK); 32 | 33 | nonces.grantRole(nonces.OPERATOR(), address(boostPXP)); 34 | 35 | vm.stopBroadcast(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /script/amoy/05_Pookyball.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.21; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { Pookyball } from "@/pookyball/Pookyball.sol"; 7 | 8 | contract DeployPookyball is Script { 9 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 10 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 11 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 12 | 13 | string baseURI = vm.envString("POOKYBALL_BASE_URI"); 14 | string contractURI = vm.envString("POOKYBALL_CONTRACT_URI"); 15 | 16 | address royaltyReceiverAddress = vm.envAddress("ROYALTY_RECEIVER_ADDRESS"); 17 | address vrfCoordinatorAddress = vm.envAddress("VRF_COORDINATOR_ADDRESS"); 18 | bytes32 vrfKeyHash = vm.envBytes32("VRF_KEY_HASH"); 19 | uint64 vrfSubId = uint64(vm.envUint("VRF_SUB_ID")); 20 | uint16 vrfMinimumRequestConfirmations = uint16(vm.envUint("VRF_MINIMUM_REQUEST_CONFIRMATIONS")); 21 | uint32 vrfCallbackGasLimit = uint32(vm.envUint("VRF_CALLBACK_GAS_LIMIT")); 22 | 23 | function run() public { 24 | // ================ Deploy Pookyball ================ 25 | vm.startBroadcast(deployerPK); 26 | 27 | Pookyball deployed = new Pookyball{ salt: salt }( 28 | baseURI, 29 | contractURI, 30 | adminAddress, 31 | royaltyReceiverAddress, 32 | vrfCoordinatorAddress, 33 | vrfKeyHash, 34 | vrfSubId, 35 | vrfMinimumRequestConfirmations, 36 | vrfCallbackGasLimit 37 | ); 38 | 39 | console.log("Pookyball deployed at:", address(deployed)); 40 | 41 | vm.stopBroadcast(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /script/amoy/06_PookyballLevelUp.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { Pookyball } from "@/pookyball/Pookyball.sol"; 7 | import { PookyballLevelUp } from "@/pookyball/PookyballLevelUp.sol"; 8 | import { POK } from "@/tokens/POK.sol"; 9 | 10 | contract DeployPookyballLevelUp is Script { 11 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 12 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 13 | uint256 adminPK = vm.envUint("ADMIN_PK"); 14 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 15 | address signerAddress = vm.envAddress("BACKEND_ADDRESS"); 16 | address treasuryAddress = vm.envAddress("TREASURY_ADDRESS"); 17 | 18 | Pookyball pookyball = Pookyball(vm.envAddress("CONTRACT_POOKYBALL_ADDRESS")); 19 | POK pok = POK(vm.envAddress("CONTRACT_POK_ADDRESS")); 20 | 21 | function run() public { 22 | vm.startBroadcast(deployerPK); 23 | 24 | PookyballLevelUp pookyballLevelUp = new PookyballLevelUp{ salt: salt }( 25 | pookyball, pok, adminAddress, signerAddress, treasuryAddress 26 | ); 27 | 28 | console.log("PookyballLevelUp deployed at:", address(pookyballLevelUp)); 29 | 30 | vm.stopBroadcast(); 31 | 32 | // ================ Admin grant roles ================ 33 | vm.startBroadcast(adminPK); 34 | 35 | console.log("POK grant roles for PookyballLevelUp"); 36 | 37 | pookyball.grantRole(pookyball.GAME(), address(pookyballLevelUp)); 38 | pok.grantRole(pok.BURNER(), address(pookyballLevelUp)); 39 | 40 | vm.stopBroadcast(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /script/amoy/07_RefillableSale.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { NonceRegistry } from "@/common/NonceRegistry.sol"; 7 | import { Pookyball } from "@/pookyball/Pookyball.sol"; 8 | import { RefillableSale } from "@/pookyball/RefillableSale.sol"; 9 | 10 | contract DeployRefillableSale is Script { 11 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 12 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 13 | uint256 adminPK = vm.envUint("ADMIN_PK"); 14 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 15 | address signerAddress = vm.envAddress("BACKEND_ADDRESS"); 16 | address treasuryAddress = vm.envAddress("TREASURY_ADDRESS"); 17 | address operatorAddress = vm.envAddress("OPERATOR_ADDRESS"); 18 | 19 | Pookyball pookyball = Pookyball(vm.envAddress("CONTRACT_POOKYBALL_ADDRESS")); 20 | 21 | function run() public { 22 | // ================ Deploy RefillableSale ================ 23 | vm.startBroadcast(deployerPK); 24 | 25 | address[] memory sellers = new address[](1); 26 | sellers[0] = operatorAddress; 27 | 28 | RefillableSale refillableSale = 29 | new RefillableSale{ salt: salt }(pookyball, treasuryAddress, adminAddress, sellers); 30 | 31 | console.log("RefillableSale deployed at:", address(refillableSale)); 32 | 33 | vm.stopBroadcast(); 34 | 35 | // ================ Admin grant roles ================ 36 | vm.startBroadcast(adminPK); 37 | 38 | console.log("Pookyball grant roles for RefillableSale to mint"); 39 | pookyball.grantRole(pookyball.MINTER(), address(refillableSale)); 40 | 41 | vm.stopBroadcast(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /script/amoy/08_Pressure.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { Pressure } from "@/pookyball/Pressure.sol"; 7 | import { POK } from "@/tokens/POK.sol"; 8 | 9 | contract DeployPressure is Script { 10 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 11 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 12 | uint256 adminPK = vm.envUint("ADMIN_PK"); 13 | address treasuryAddress = vm.envAddress("TREASURY_ADDRESS"); 14 | 15 | POK pok = POK(vm.envAddress("CONTRACT_POK_ADDRESS")); 16 | 17 | function run() public { 18 | // ================ Deploy Pressure ================ 19 | vm.startBroadcast(deployerPK); 20 | 21 | Pressure pressure = new Pressure{ salt: salt }(pok, treasuryAddress); 22 | 23 | console.log("Pressure deployed at:", address(pressure)); 24 | 25 | vm.stopBroadcast(); 26 | 27 | // ================ Admin grant roles ================ 28 | vm.startBroadcast(adminPK); 29 | 30 | console.log("Pookyball grant roles for Pressure to burn POK"); 31 | pok.grantRole(pok.BURNER(), address(pressure)); 32 | 33 | vm.stopBroadcast(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /script/amoy/09_PookyballReroll.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.21; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { NonceRegistry } from "@/common/NonceRegistry.sol"; 7 | import { IPookyball } from "@/pookyball/IPookyball.sol"; 8 | import { PookyballReroll } from "@/pookyball/PookyballReroll.sol"; 9 | 10 | contract DeployPookyballReroll is Script { 11 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 12 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 13 | uint256 adminPK = vm.envUint("ADMIN_PK"); 14 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 15 | address signerAddress = vm.envAddress("BACKEND_ADDRESS"); 16 | address treasuryAddress = vm.envAddress("TREASURY_ADDRESS"); 17 | 18 | NonceRegistry nonces = NonceRegistry(vm.envAddress("CONTRACT_NONCE_REGISTRY_ADDRESS")); 19 | IPookyball pookyball = IPookyball(vm.envAddress("CONTRACT_POOKYBALL_ADDRESS")); 20 | 21 | function run() public { 22 | // ================ Deploy PookyballReroll ================ 23 | vm.startBroadcast(deployerPK); 24 | 25 | PookyballReroll reroll = new PookyballReroll{ salt: salt }( 26 | pookyball, nonces, adminAddress, signerAddress, treasuryAddress 27 | ); 28 | 29 | console.log("PookyballReroll deployed at:", address(reroll)); 30 | 31 | vm.stopBroadcast(); 32 | 33 | // ================ Admin grant roles ================ 34 | vm.startBroadcast(adminPK); 35 | 36 | nonces.grantRole(nonces.OPERATOR(), address(reroll)); 37 | 38 | vm.stopBroadcast(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /script/amoy/09_Stickers.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { Stickers } from "@/stickers/Stickers.sol"; 7 | import { VRFConfig } from "@/types/VRFConfig.sol"; 8 | import { VRFCoordinatorV2Interface } from "chainlink/interfaces/VRFCoordinatorV2Interface.sol"; 9 | 10 | contract DeployStickers is Script { 11 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 12 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 13 | uint256 adminPK = vm.envUint("ADMIN_PK"); 14 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 15 | 16 | address vrfCoordinatorAddress = vm.envAddress("VRF_COORDINATOR_ADDRESS"); 17 | bytes32 vrfKeyHash = vm.envBytes32("VRF_KEY_HASH"); 18 | uint64 vrfSubId = uint64(vm.envUint("VRF_SUB_ID")); 19 | uint16 vrfMinimumRequestConfirmations = uint16(vm.envUint("VRF_MINIMUM_REQUEST_CONFIRMATIONS")); 20 | uint32 vrfCallbackGasLimit = uint32(vm.envUint("VRF_CALLBACK_GAS_LIMIT")); 21 | 22 | address royaltyReceiverAddress = vm.envAddress("ROYALTY_RECEIVER_ADDRESS"); 23 | 24 | VRFConfig vrf = VRFConfig({ 25 | coordinator: VRFCoordinatorV2Interface(vrfCoordinatorAddress), 26 | keyHash: vrfKeyHash, 27 | subcriptionId: vrfSubId, 28 | minimumRequestConfirmations: vrfMinimumRequestConfirmations, 29 | callbackGasLimit: vrfCallbackGasLimit 30 | }); 31 | 32 | function run() external { 33 | // ================ Deploy Stickers ================ 34 | vm.startBroadcast(deployerPK); 35 | 36 | Stickers stickers = new Stickers{ salt: salt }(adminAddress, royaltyReceiverAddress, vrf); 37 | console.log("Stickers deployed at:", address(stickers)); 38 | 39 | vm.stopBroadcast(); 40 | 41 | uint256 chainID = vm.envUint("CHAIN_ID"); 42 | if (chainID != 31337 && chainID != 80002) { 43 | // ================ Admin grant roles ================ 44 | vm.startBroadcast(adminPK); 45 | 46 | console.log("Add the Stickers consumer to VRF Coordinator"); 47 | vrf.coordinator.addConsumer(vrf.subcriptionId, address(stickers)); 48 | 49 | vm.stopBroadcast(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /script/amoy/12_StickersLevelUp.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "forge-std/console.sol"; 5 | import { IStickers } from "@/stickers/Stickers.sol"; 6 | import { POK } from "@/tokens/POK.sol"; 7 | import { Script } from "forge-std/Script.sol"; 8 | import { Stickers } from "@/stickers/Stickers.sol"; 9 | import { StickersLevelUp } from "@/stickers/StickersLevelUp.sol"; 10 | 11 | contract DeployStickersLevelUp is Script { 12 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 13 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 14 | uint256 adminPK = vm.envUint("ADMIN_PK"); 15 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 16 | address signerAddress = vm.envAddress("BACKEND_ADDRESS"); 17 | 18 | address royaltyReceiverAddress = vm.envAddress("ROYALTY_RECEIVER_ADDRESS"); 19 | address stickersAddress = vm.envAddress("CONTRACT_STICKERS_ADDRESS"); 20 | POK pok = POK(vm.envAddress("CONTRACT_POK_ADDRESS")); 21 | 22 | function run() external { 23 | // ================ Deploy StickersLevelUp ================ 24 | vm.startBroadcast(deployerPK); 25 | 26 | StickersLevelUp stickersLevelUp = new StickersLevelUp{ salt: salt }( 27 | IStickers(stickersAddress), pok, adminAddress, signerAddress, royaltyReceiverAddress 28 | ); 29 | 30 | console.log("StickersLevelUp deployed at:", address(stickersLevelUp)); 31 | 32 | vm.stopBroadcast(); 33 | 34 | // ================ Admin grant roles ================ 35 | vm.startBroadcast(adminPK); 36 | 37 | console.log("POK grant roles for StickersLevelUp"); 38 | pok.grantRole(pok.BURNER(), address(stickersLevelUp)); 39 | 40 | vm.stopBroadcast(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /script/amoy/13_StickersSale.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { Stickers } from "@/stickers/Stickers.sol"; 7 | import { StickersSale, Pack, PackContent } from "@/stickers/StickersSale.sol"; 8 | 9 | contract DeployStickersSale is Script { 10 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 11 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 12 | uint256 adminPK = vm.envUint("ADMIN_PK"); 13 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 14 | 15 | address royaltyReceiverAddress = vm.envAddress("ROYALTY_RECEIVER_ADDRESS"); 16 | address stickersAddress = vm.envAddress("CONTRACT_STICKERS_ADDRESS"); 17 | Stickers stickers = Stickers(stickersAddress); 18 | 19 | function run() external { 20 | Pack[] memory stickersPacks = new Pack[](4); 21 | stickersPacks[0] = Pack({ 22 | price: 8 ether / 1e6, 23 | supply: 45, 24 | minted: 0, 25 | totalSupply: 45, 26 | content: PackContent({ common: 2, rare: 0, epic: 0, legendary: 0 }) 27 | }); 28 | stickersPacks[1] = Pack({ 29 | price: 28 ether / 1e6, 30 | supply: 40, 31 | minted: 0, 32 | totalSupply: 40, 33 | content: PackContent({ common: 3, rare: 1, epic: 0, legendary: 0 }) 34 | }); 35 | stickersPacks[2] = Pack({ 36 | price: 112 ether / 1e6, 37 | supply: 12, 38 | minted: 0, 39 | totalSupply: 12, 40 | content: PackContent({ common: 8, rare: 1, epic: 1, legendary: 0 }) 41 | }); 42 | stickersPacks[3] = Pack({ 43 | price: 416 ether / 1e6, 44 | supply: 3, 45 | minted: 0, 46 | totalSupply: 3, 47 | content: PackContent({ common: 16, rare: 2, epic: 1, legendary: 1 }) 48 | }); 49 | 50 | // ================ Deploy StickersSale ================ 51 | vm.startBroadcast(deployerPK); 52 | 53 | StickersSale stickersSale = 54 | new StickersSale{ salt: salt }(stickers, adminAddress, royaltyReceiverAddress, stickersPacks); 55 | 56 | console.log("StickersSale deployed at:", address(stickersSale)); 57 | 58 | vm.stopBroadcast(); 59 | 60 | // ================ Admin grant roles ================ 61 | vm.startBroadcast(adminPK); 62 | 63 | console.log("Stickers grant roles for StickerSale"); 64 | stickers.grantRoles(address(stickersSale), stickers.MINTER()); 65 | 66 | vm.stopBroadcast(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /script/amoy/14_StickersController.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "forge-std/console.sol"; 5 | import { IPookyball } from "@/pookyball/IPookyball.sol"; 6 | import { IStickers } from "@/stickers/Stickers.sol"; 7 | import { Script } from "forge-std/Script.sol"; 8 | import { StickersController } from "@/stickers/StickersController.sol"; 9 | 10 | contract DeployStickersController is Script { 11 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 12 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 13 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 14 | 15 | address pookyballAddress = vm.envAddress("CONTRACT_POOKYBALL_ADDRESS"); 16 | address stickersAddress = vm.envAddress("CONTRACT_STICKERS_ADDRESS"); 17 | 18 | function run() external { 19 | // ================ Deploy StickersController ================ 20 | vm.startBroadcast(deployerPK); 21 | 22 | StickersController controller = new StickersController{ salt: salt }( 23 | IPookyball(pookyballAddress), IStickers(stickersAddress), adminAddress 24 | ); 25 | 26 | console.log("StickersController deployed at:", address(controller)); 27 | 28 | vm.stopBroadcast(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /script/amoy/15_StickersManager.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { StickersController } from "@/stickers/StickersController.sol"; 7 | import { StickersManager } from "@/stickers/StickersManager.sol"; 8 | 9 | contract DeployStickersManager is Script { 10 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 11 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 12 | uint256 adminPK = vm.envUint("ADMIN_PK"); 13 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 14 | 15 | address stickersControllerAddress = vm.envAddress("CONTRACT_STICKERS_CONTROLLER_ADDRESS"); 16 | StickersController controller = StickersController(stickersControllerAddress); 17 | 18 | function run() external { 19 | // ================ Deploy StickersManager ================ 20 | vm.startBroadcast(deployerPK); 21 | 22 | StickersManager manager = new StickersManager{ salt: salt }(controller); 23 | 24 | console.log("StickersManager deployed at:", address(manager)); 25 | 26 | vm.stopBroadcast(); 27 | 28 | // ================ Admin grant roles ================ 29 | vm.startBroadcast(adminPK); 30 | 31 | console.log("StickerController grant roles for StickerManager"); 32 | controller.grantRoles(address(manager), controller.LINKER() | controller.REPLACER()); 33 | 34 | vm.stopBroadcast(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /script/amoy/16_StickersAscension.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "forge-std/console.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { Stickers } from "@/stickers/Stickers.sol"; 7 | import { StickersController } from "@/stickers/StickersController.sol"; 8 | import { StickersAscension } from "@/stickers/StickersAscension.sol"; 9 | 10 | contract DeployStickersAscension is Script { 11 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 12 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 13 | uint256 adminPK = vm.envUint("ADMIN_PK"); 14 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 15 | address signerAddress = vm.envAddress("BACKEND_ADDRESS"); 16 | 17 | address stickersAddress = vm.envAddress("CONTRACT_STICKERS_ADDRESS"); 18 | address stickersControllerAddress = vm.envAddress("CONTRACT_STICKERS_CONTROLLER_ADDRESS"); 19 | StickersController controller = StickersController(stickersControllerAddress); 20 | Stickers stickers = Stickers(stickersAddress); 21 | 22 | function run() public { 23 | // ================ Deploy StickersAscension ================ 24 | vm.startBroadcast(deployerPK); 25 | 26 | StickersAscension ascension = 27 | new StickersAscension{ salt: salt }(controller, adminAddress, signerAddress); 28 | console.log("StickersAscension deployed at:", address(ascension)); 29 | 30 | vm.stopBroadcast(); 31 | 32 | // ================ Admin grant roles ================ 33 | vm.startBroadcast(adminPK); 34 | 35 | console.log("StickersController grant roles for StickersAscension"); 36 | 37 | controller.grantRoles(address(ascension), controller.REMOVER()); 38 | 39 | console.log("Stickers grant roles for StickersAscension"); 40 | stickers.grantRoles(address(ascension), stickers.MINTER()); 41 | 42 | vm.stopBroadcast(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /script/amoy/17_PookyballAscension.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "forge-std/console.sol"; 5 | import { Pookyball } from "@/pookyball/Pookyball.sol"; 6 | import { PookyballAscension } from "@/pookyball/PookyballAscension.sol"; 7 | import { Script } from "forge-std/Script.sol"; 8 | import { StickersController } from "@/stickers/StickersController.sol"; 9 | 10 | contract DeployPookyballAscension is Script { 11 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 12 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 13 | uint256 adminPK = vm.envUint("ADMIN_PK"); 14 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 15 | address signerAddress = vm.envAddress("BACKEND_ADDRESS"); 16 | address treasuryAddress = vm.envAddress("TREASURY_ADDRESS"); 17 | 18 | address stickersControllerAddress = vm.envAddress("CONTRACT_STICKERS_CONTROLLER_ADDRESS"); 19 | StickersController controller = StickersController(stickersControllerAddress); 20 | Pookyball pookyball = Pookyball(vm.envAddress("CONTRACT_POOKYBALL_ADDRESS")); 21 | 22 | function run() public { 23 | // ================ Deploy PookyballAscension ================ 24 | vm.startBroadcast(deployerPK); 25 | 26 | PookyballAscension pookyballAscension = new PookyballAscension{ salt: salt }( 27 | pookyball, controller, adminAddress, signerAddress, treasuryAddress 28 | ); 29 | 30 | console.log("PookyballAscension deployed at:", address(pookyballAscension)); 31 | 32 | vm.stopBroadcast(); 33 | 34 | // ================ Admin grant roles ================ 35 | vm.startBroadcast(adminPK); 36 | 37 | console.log("Pookyball grant roles for PookyballAscension"); 38 | pookyball.grantRole(pookyball.MINTER(), address(pookyballAscension)); 39 | 40 | console.log("StickersController grant roles for PookyballAscension"); 41 | controller.grantRoles(address(pookyballAscension), controller.REMOVER()); 42 | 43 | vm.stopBroadcast(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /script/amoy/18_Rewards.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import "forge-std/console.sol"; 5 | import { NonceRegistry } from "@/common/NonceRegistry.sol"; 6 | import { POK } from "@/tokens/POK.sol"; 7 | import { Pookyball } from "@/pookyball/Pookyball.sol"; 8 | import { Rewards } from "@/common/Rewards.sol"; 9 | import { Script } from "forge-std/Script.sol"; 10 | import { Stickers } from "@/stickers/Stickers.sol"; 11 | 12 | contract DeployRewards is Script { 13 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 14 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 15 | uint256 adminPK = vm.envUint("ADMIN_PK"); 16 | address adminAddress = vm.envAddress("ADMIN_ADDRESS"); 17 | address operatorAddress = vm.envAddress("OPERATOR_ADDRESS"); 18 | 19 | Pookyball pookyball = Pookyball(vm.envAddress("CONTRACT_POOKYBALL_ADDRESS")); 20 | Stickers stickers = Stickers(vm.envAddress("CONTRACT_STICKERS_ADDRESS")); 21 | POK pok = POK(vm.envAddress("CONTRACT_POK_ADDRESS")); 22 | NonceRegistry nonces = NonceRegistry(vm.envAddress("CONTRACT_NONCE_REGISTRY_ADDRESS")); 23 | 24 | function run() external { 25 | // ================ Deploy Rewards ================ 26 | vm.startBroadcast(deployerPK); 27 | 28 | address[] memory rewarders = new address[](1); 29 | rewarders[0] = operatorAddress; 30 | 31 | Rewards rewards = 32 | new Rewards{ salt: salt }(pok, pookyball, stickers, nonces, adminAddress, rewarders); 33 | 34 | console.log("Rewards deployed at:", address(rewards)); 35 | 36 | vm.stopBroadcast(); 37 | 38 | // ================ Admin grant roles ================ 39 | vm.startBroadcast(adminPK); 40 | 41 | nonces.grantRole(nonces.OPERATOR(), address(rewards)); 42 | 43 | pookyball.grantRole(pookyball.MINTER(), address(rewards)); 44 | 45 | stickers.grantRoles(address(rewards), stickers.MINTER()); 46 | 47 | pok.grantRole(pok.MINTER(), address(rewards)); 48 | 49 | vm.stopBroadcast(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /script/artifacts.ts: -------------------------------------------------------------------------------- 1 | import glob from "fast-glob"; 2 | import { basename, dirname } from "node:path"; 3 | import { exec as execAsync } from "node:child_process"; 4 | import { mkdir, writeFile } from "node:fs/promises"; 5 | import { promisify } from "util"; 6 | 7 | const exec = promisify(execAsync); 8 | 9 | /** 10 | * Generate the contract json ABI. 11 | * @param contract The contract path. 12 | */ 13 | async function abi(contract: string): Promise { 14 | const name = basename(contract).replace(/\.sol$/, ""); 15 | const output = contract.replace(/^src/, "abi").replace(/\.sol$/, ".json"); 16 | await mkdir(dirname(output), { recursive: true }); 17 | 18 | const { stdout } = await exec(`forge inspect ${contract}:${name} abi`); 19 | await writeFile(output, stdout); 20 | } 21 | 22 | /** 23 | * Generate the contract bytecode. 24 | * @param contract The contract path. 25 | */ 26 | async function bytecode(contract: string): Promise { 27 | const name = basename(contract).replace(/\.sol$/, ""); 28 | const output = contract.replace(/^src/, "bytecode").replace(/\.sol$/, ".bin"); 29 | await mkdir(dirname(output), { recursive: true }); 30 | 31 | const { stdout } = await exec(`forge inspect ${contract}:${name} bytecode`); 32 | await writeFile(output, stdout); 33 | } 34 | 35 | // Get all contracts 36 | let contracts = await glob("src/**/*.sol"); 37 | // Filter out types directory 38 | contracts = contracts.filter((contract) => !contract.includes("/types/")); 39 | // Filter out interfaces 40 | contracts = contracts.filter((contract) => !contract.includes("/I")); 41 | 42 | const jobs = contracts.reduce[]>((acc, contract) => { 43 | console.log(contract); 44 | acc.push(abi(contract), bytecode(contract)); 45 | return acc; 46 | }, []); 47 | 48 | await Promise.all(jobs); 49 | -------------------------------------------------------------------------------- /script/comments.ts: -------------------------------------------------------------------------------- 1 | import glob from 'fast-glob'; 2 | import { readFile, writeFile } from 'node:fs/promises'; 3 | 4 | // This script migrates /* */ comments to /// 5 | 6 | async function run(file: string) { 7 | const code = await readFile(file, 'utf-8'); 8 | const patch = code 9 | .replace(/^ *\/\*\*$/gm, '~~~') 10 | .replace(/^ +\*\/$/gm, '~~~') 11 | .replaceAll('~~~\n', '') 12 | .replace(/^ ( *)\*( ?)/gm, '$1///$2'); 13 | await writeFile(file, patch); 14 | } 15 | 16 | const contracts = await glob('{src,script,test}/**/*.sol'); 17 | const jobs = contracts.map((file) => run(file)); 18 | 19 | await Promise.all(jobs); 20 | -------------------------------------------------------------------------------- /script/polygon/01_Stickers.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.21; 3 | 4 | import { VRFCoordinatorV2Interface } from "chainlink/interfaces/VRFCoordinatorV2Interface.sol"; 5 | import { Script } from "forge-std/Script.sol"; 6 | import { INonceRegistry } from "@/common/INonceRegistry.sol"; 7 | import { IPookyball } from "@/pookyball/IPookyball.sol"; 8 | import { Stickers } from "@/stickers/Stickers.sol"; 9 | import { StickersController } from "@/stickers/StickersController.sol"; 10 | import { StickersLevelUp } from "@/stickers/StickersLevelUp.sol"; 11 | import { StickersManager } from "@/stickers/StickersManager.sol"; 12 | import { StickersSale, Pack, PackContent } from "@/stickers/StickersSale.sol"; 13 | import { POK } from "@/tokens/POK.sol"; 14 | import { VRFConfig } from "@/types/VRFConfig.sol"; 15 | 16 | struct StickersConfig { 17 | VRFConfig vrf; 18 | Pack[] packs; 19 | address tSale; 20 | address tERC2981; 21 | address tLevelUp; 22 | } 23 | 24 | struct Config { 25 | IPookyball pookyball; 26 | POK pok; 27 | address admin; 28 | address signer; 29 | StickersConfig st; 30 | } 31 | 32 | contract DeployStickers is Script { 33 | function polygon() internal pure returns (Config memory) { 34 | Pack[] memory packs = new Pack[](4); 35 | packs[0] = Pack({ 36 | price: 14 ether, 37 | supply: 0, 38 | minted: 0, 39 | totalSupply: 0, 40 | content: PackContent({ common: 2, rare: 0, epic: 0, legendary: 0 }) 41 | }); 42 | packs[1] = Pack({ 43 | price: 49 ether, 44 | supply: 0, 45 | minted: 0, 46 | totalSupply: 0, 47 | content: PackContent({ common: 3, rare: 1, epic: 0, legendary: 0 }) 48 | }); 49 | packs[2] = Pack({ 50 | price: 196, 51 | supply: 0, 52 | minted: 0, 53 | totalSupply: 0, 54 | content: PackContent({ common: 8, rare: 1, epic: 1, legendary: 0 }) 55 | }); 56 | packs[3] = Pack({ 57 | price: 749, 58 | supply: 0, 59 | minted: 0, 60 | totalSupply: 0, 61 | content: PackContent({ common: 15, rare: 3, epic: 1, legendary: 1 }) 62 | }); 63 | 64 | return Config({ 65 | pookyball: IPookyball(0xb4859acd9B0A65CA4897c31e5cb5160D9Ff32C0A), 66 | pok: POK(0x7b7E3B03f34b17d70C276C4886467D58867Bbc94), 67 | admin: 0x3CC4F4372F83ad3C577eD6e1Aae3D244A1b955D5, 68 | signer: 0xCAFE3e690bf74Ec274210E1c448130c1f8228513, 69 | st: StickersConfig({ 70 | vrf: VRFConfig({ 71 | coordinator: VRFCoordinatorV2Interface(0xAE975071Be8F8eE67addBC1A82488F1C24858067), 72 | keyHash: 0xcc294a196eeeb44da2888d17c0625cc88d70d9760a69d58d853ba6581a9ab0cd, 73 | subcriptionId: 586, 74 | minimumRequestConfirmations: 10, 75 | callbackGasLimit: 2500000 76 | }), 77 | packs: packs, 78 | tSale: 0x96224B6a800294F40c547f7Ec0952eA222526040, 79 | tERC2981: 0x598895F50951186eFdCB160764a538f353894027, 80 | tLevelUp: 0x703662853D7F9ad9D8c44128222266a736741437 81 | }) 82 | }); 83 | } 84 | 85 | function run() external { 86 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 87 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 88 | address deployer = vm.addr(deployerPK); 89 | 90 | vm.startBroadcast(deployerPK); 91 | Config memory c = polygon(); 92 | 93 | Stickers stickers = new Stickers{salt: salt}(deployer, c.st.tERC2981, c.st.vrf); 94 | StickersController controller = 95 | new StickersController{salt: salt}(c.pookyball, stickers, deployer); 96 | StickersManager manager = new StickersManager{salt: salt}(controller); 97 | new StickersLevelUp{salt: salt}(stickers, c.pok, c.admin, c.signer, c.st.tLevelUp); 98 | StickersSale stickersSale = 99 | new StickersSale{salt: salt}(stickers, c.admin, c.st.tSale, c.st.packs); 100 | 101 | // StickersManager 102 | controller.grantRoles(address(manager), controller.LINKER() | controller.REPLACER()); 103 | controller.transferOwnership(c.admin); 104 | 105 | // StickersSale 106 | stickers.grantRoles(address(stickersSale), stickers.MINTER()); 107 | stickers.transferOwnership(c.admin); 108 | 109 | vm.stopBroadcast(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /script/polygon/02_PookyballAscension.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import { Script } from "forge-std/Script.sol"; 5 | import { NonceRegistry } from "@/common/NonceRegistry.sol"; 6 | import { Rewards } from "@/common/Rewards.sol"; 7 | import { Pookyball } from "@/pookyball/Pookyball.sol"; 8 | import { PookyballAscension } from "@/pookyball/PookyballAscension.sol"; 9 | import { StickersController } from "@/stickers/StickersController.sol"; 10 | 11 | contract DeployPookyballAscension is Script { 12 | Pookyball pookyball = Pookyball(0xb4859acd9B0A65CA4897c31e5cb5160D9Ff32C0A); 13 | StickersController controller = StickersController(0x75cc3c6329930758659eD87338B926c90e16d05F); 14 | 15 | address signer = 0xCAFE3e690bf74Ec274210E1c448130c1f8228513; 16 | address admin = 0x3CC4F4372F83ad3C577eD6e1Aae3D244A1b955D5; 17 | address treasury = 0x703662853D7F9ad9D8c44128222266a736741437; 18 | 19 | function run() public { 20 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 21 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 22 | 23 | vm.startBroadcast(deployerPK); 24 | new PookyballAscension{salt: salt}(pookyball, controller, admin, signer, treasury); 25 | vm.stopBroadcast(); 26 | 27 | // run following tx: 28 | // pookyball.grantRole(pookyball.MINTER(), address(ascension)); 29 | // controller.grantRoles(address(ascension), controller.REMOVER()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /script/polygon/03_PookyballLevelUp.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import { Script } from "forge-std/Script.sol"; 5 | import { NonceRegistry } from "@/common/NonceRegistry.sol"; 6 | import { Pookyball } from "@/pookyball/Pookyball.sol"; 7 | import { PookyballLevelUp } from "@/pookyball/PookyballLevelUp.sol"; 8 | import { StickersController } from "@/stickers/StickersController.sol"; 9 | import { POK } from "@/tokens/POK.sol"; 10 | 11 | contract DeployPookyballLevelUp is Script { 12 | POK pok = POK(0x7b7E3B03f34b17d70C276C4886467D58867Bbc94); 13 | Pookyball pookyball = Pookyball(0xb4859acd9B0A65CA4897c31e5cb5160D9Ff32C0A); 14 | 15 | address signer = 0xCAFE3e690bf74Ec274210E1c448130c1f8228513; 16 | address admin = 0x3CC4F4372F83ad3C577eD6e1Aae3D244A1b955D5; 17 | address treasury = 0x703662853D7F9ad9D8c44128222266a736741437; 18 | 19 | function run() public { 20 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 21 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 22 | 23 | vm.startBroadcast(deployerPK); 24 | new PookyballLevelUp{salt: salt}(pookyball, pok, admin, signer, treasury); 25 | vm.stopBroadcast(); 26 | 27 | // run following tx: 28 | // pookyball.grantRole(pookyball.GAME(), address(levelUp)); 29 | // pok.grantRole(pok.BURNER(), address(levelUp)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /script/polygon/04_PookyballReroll.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.21; 3 | 4 | import { Script } from "forge-std/Script.sol"; 5 | import { NonceRegistry } from "@/common/NonceRegistry.sol"; 6 | import { IPookyball } from "@/pookyball/IPookyball.sol"; 7 | import { PookyballReroll } from "@/pookyball/PookyballReroll.sol"; 8 | 9 | contract DeployPookyballReroll is Script { 10 | NonceRegistry nonces = NonceRegistry(0xB08Ee469Dcf9c40B77261d8665A8BbdFad22B818); 11 | IPookyball pookyball = IPookyball(0xb4859acd9B0A65CA4897c31e5cb5160D9Ff32C0A); 12 | address admin = 0x3CC4F4372F83ad3C577eD6e1Aae3D244A1b955D5; 13 | address signer = 0xCAFE3e690bf74Ec274210E1c448130c1f8228513; 14 | address treasury = 0x703662853D7F9ad9D8c44128222266a736741437; 15 | 16 | function run() public { 17 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 18 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 19 | 20 | vm.startBroadcast(deployerPK); 21 | new PookyballReroll{salt: salt}(pookyball, nonces, admin, signer, treasury); 22 | vm.stopBroadcast(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /script/polygon/06_StickersAscension.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.22; 3 | 4 | import { Script } from "forge-std/Script.sol"; 5 | import { Stickers } from "@/stickers/Stickers.sol"; 6 | import { StickersController } from "@/stickers/StickersController.sol"; 7 | import { StickersAscension } from "@/stickers/StickersAscension.sol"; 8 | 9 | contract DeployPookyballAscension is Script { 10 | StickersController controller = StickersController(0x75cc3c6329930758659eD87338B926c90e16d05F); 11 | Stickers stickers = Stickers(address(controller.stickers())); 12 | 13 | address signer = 0xCAFE3e690bf74Ec274210E1c448130c1f8228513; 14 | 15 | function run() public { 16 | bytes32 salt = keccak256(bytes(vm.envString("SALT"))); 17 | uint256 deployerPK = vm.envUint("DEPLOYER_PK"); 18 | uint256 adminPK = vm.envUint("ADMIN_PK"); 19 | address admin = vm.addr(adminPK); 20 | 21 | vm.startBroadcast(deployerPK); 22 | new StickersAscension{salt: salt}(controller, admin, signer); 23 | vm.stopBroadcast(); 24 | 25 | // vm.startBroadcast(adminPK); 26 | // stickers.grantRoles(address(ascension), stickers.MINTER()); 27 | // controller.grantRoles(address(ascension), controller.REMOVER()); 28 | // vm.stopBroadcast(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /script/shell/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NETWORK="$1" 4 | CONTRACT_NAME="$2" 5 | 6 | # Dynamically determine the list of allowed contracts by scanning for .s.sol files 7 | # Assuming all contract files are in the script/amoy folder and follow the naming convention *_.s.sol 8 | # ALLOWED_CONTRACTS=($(find script/amoy -type f -name "*.s.sol" | sed -E 's|script/amoy/.*_([^.]+)\.s\.sol|\1|')) 9 | ALLOWED_CONTRACTS=( 10 | "POK" 11 | "Energy" 12 | "NonceRegistry" 13 | "BoostPXP" 14 | "Pookyball" 15 | "PookyballLevelUp" 16 | "RefillableSale" 17 | "Pressure" 18 | "PookyballReroll" 19 | "Stickers" 20 | "StickersLevelUp" 21 | "StickersSale" 22 | "StickersController" 23 | "StickersManager" 24 | "StickersAscension" 25 | "PookyballAscension" 26 | "Rewards" 27 | ) 28 | 29 | # Define the networks 30 | NETWORKS=("local" "amoy" "mainnet") 31 | 32 | # Usage message 33 | function usage { 34 | echo "Usage: $0 {network} {ContractName}" 35 | echo "Networks: ${NETWORKS[*]}" 36 | echo "Allowed contracts: ${ALLOWED_CONTRACTS[*]}" 37 | echo "Examples:" 38 | # Iterate over each contract 39 | for contract in "${ALLOWED_CONTRACTS[@]}"; do 40 | 41 | # Display the example usage for the current contract on the current network 42 | if [[ " ${NETWORKS[@]} " =~ " ${NETWORK} " ]]; then 43 | echo "$0 $NETWORK $contract" 44 | else 45 | echo "$0 local $contract" 46 | fi 47 | done 48 | exit 1 49 | } 50 | 51 | # Check if two arguments were provided 52 | if [ "$#" -ne 2 ]; then 53 | usage 54 | fi 55 | 56 | # Check if the contract name is allowed 57 | if [[ ! " ${ALLOWED_CONTRACTS[@]} " =~ " ${CONTRACT_NAME} " ]]; then 58 | echo "Invalid contract name: ${CONTRACT_NAME}" 59 | echo "Allowed contracts are: ${ALLOWED_CONTRACTS[*]}" 60 | exit 2 61 | fi 62 | 63 | ENV_FILE=".env.${NETWORK}" 64 | 65 | # Execute the command 66 | export $(cat $ENV_FILE | sed 's/\r$//' | xargs) 67 | 68 | # Assuming $RPC_URL is set in the environment variables for amoy and mainnet 69 | # and possibly a different URL for local (e.g., http://localhost:8545) 70 | if [[ "$NETWORK" == "local" ]]; then 71 | DEPLOYMENT_SCRIPTS="script/amoy" 72 | RPC_URL_ARG="--fork-url $RPC_URL" 73 | else 74 | DEPLOYMENT_SCRIPTS="script/$NETWORK" 75 | RPC_URL_ARG="--rpc-url $RPC_URL" 76 | fi 77 | 78 | # Dynamically find the contract file 79 | CONTRACT_FILE=$(find $DEPLOYMENT_SCRIPTS -type f -name "*_${CONTRACT_NAME}.s.sol") 80 | 81 | if [ -z "$CONTRACT_FILE" ]; then 82 | echo "Contract file for ${CONTRACT_NAME} not found." 83 | exit 4 84 | fi 85 | 86 | # Execute the forge script 87 | forge script $CONTRACT_FILE --tc Deploy$CONTRACT_NAME $RPC_URL_ARG --broadcast 88 | 89 | # Flatten the contract whru flatten ( ./flatten.sh: No such file or directory ) 90 | $PWD/script/shell/flatten.sh $CONTRACT_NAME -------------------------------------------------------------------------------- /script/shell/flatten.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ALLOWED_CONTRACTS=( 4 | "POK" 5 | "Energy" 6 | "NonceRegistry" 7 | "BoostPXP" 8 | "Pookyball" 9 | "PookyballLevelUp" 10 | "RefillableSale" 11 | "Pressure" 12 | "PookyballReroll" 13 | "PookyballAscension" 14 | "Stickers" 15 | "StickersLevelUp" 16 | "StickersSale" 17 | "StickersController" 18 | "StickersManager" 19 | "StickersAscension" 20 | "Rewards" 21 | ) 22 | 23 | # Usage message 24 | function usage { 25 | echo "Usage: $0 {ContractName}" 26 | echo "Allowed contracts: ${ALLOWED_CONTRACTS[*]}" 27 | exit 1 28 | } 29 | 30 | CONTRACT_NAME=$1 31 | 32 | if [[ ! " ${ALLOWED_CONTRACTS[@]} " =~ " ${CONTRACT_NAME} " ]]; then 33 | echo "Contract not allowed" 34 | exit 1 35 | fi 36 | 37 | case $CONTRACT_NAME in 38 | "POK") 39 | CONTRACT_PATH="src/tokens/POK.sol" 40 | ;; 41 | "Energy") 42 | CONTRACT_PATH="src/common/Energy.sol" 43 | ;; 44 | "NonceRegistry") 45 | CONTRACT_PATH="src/common/NonceRegistry.sol" 46 | ;; 47 | "BoostPXP") 48 | CONTRACT_PATH="src/common/BoostPXP.sol" 49 | ;; 50 | "Pookyball") 51 | CONTRACT_PATH="src/pookyball/Pookyball.sol" 52 | ;; 53 | "PookyballLevelUp") 54 | CONTRACT_PATH="src/pookyball/PookyballLevelUp.sol" 55 | ;; 56 | "RefillableSale") 57 | CONTRACT_PATH="src/pookyball/RefillableSale.sol" 58 | ;; 59 | "Pressure") 60 | CONTRACT_PATH="src/pookyball/Pressure.sol" 61 | ;; 62 | "PookyballReroll") 63 | CONTRACT_PATH="src/pookyball/PookyballReroll.sol" 64 | ;; 65 | "PookyballAscension") 66 | CONTRACT_PATH="src/pookyball/PookyballAscension.sol" 67 | ;; 68 | "Stickers") 69 | CONTRACT_PATH="src/stickers/Stickers.sol" 70 | ;; 71 | "StickersLevelUp") 72 | CONTRACT_PATH="src/stickers/StickersLevelUp.sol" 73 | ;; 74 | "StickersSale") 75 | CONTRACT_PATH="src/stickers/StickersSale.sol" 76 | ;; 77 | "StickersController") 78 | CONTRACT_PATH="src/stickers/StickersController.sol" 79 | ;; 80 | "StickersManager") 81 | CONTRACT_PATH="src/stickers/StickersManager.sol" 82 | ;; 83 | "StickersAscension") 84 | CONTRACT_PATH="src/stickers/StickersAscension.sol" 85 | ;; 86 | "Rewards") 87 | CONTRACT_PATH="src/common/Rewards.sol" 88 | ;; 89 | *) 90 | echo "Contract not allowed" 91 | exit 1 92 | ;; 93 | esac 94 | 95 | 96 | forge flatten $CONTRACT_PATH --output flattened/$CONTRACT_NAME.sol 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/common/BoostPXP.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; 5 | import { INonceRegistry } from "@/common/INonceRegistry.sol"; 6 | import { Signer } from "@/common/Signer.sol"; 7 | import { Treasury } from "@/common/Treasury.sol"; 8 | import { PookyballRarity, IPookyball } from "@/pookyball/IPookyball.sol"; 9 | 10 | struct Boost { 11 | /// The Pookyball rarity. 12 | PookyballRarity rarity; 13 | /// The number of boosted predictions. 14 | uint256 predictions; 15 | /// The boost value. 16 | uint256 value; 17 | } 18 | 19 | /// @title BoostPXP 20 | /// @author Mathieu Bour for Pooky Labs Ltd. 21 | /// Allow player to purchase temporary PXP boosts for their Pookyball. 22 | contract BoostPXP is OwnableRoles, Signer, Treasury { 23 | /// Fired when a boost has been purchased. 24 | /// @param account The account who purchased the boost. 25 | /// @param details The boost details. 26 | /// @param price The amount in native currency paid for the boost. 27 | event Boosted(address indexed account, Boost details, uint256 price); 28 | 29 | /// Thrown when user attempts to use a nonce that was already used in the past. 30 | error NonceAlreadyUsed(bytes32 nonce); 31 | 32 | /// The NonceRegistry contract. 33 | INonceRegistry public immutable nonces; 34 | 35 | constructor(INonceRegistry _nonces, address admin, address _signer, address _treasury) 36 | Signer(_signer) 37 | Treasury(_treasury) 38 | { 39 | _initializeOwner(admin); 40 | nonces = _nonces; 41 | } 42 | 43 | /// @notice Boost the received PXP of a Pookyball. 44 | /// @dev Pricing is controlled by the backend, which need to provide a proof to the end user. 45 | /// @param details The Pookyball boost data. 46 | /// @param price The price in native currency, provided by the Pooky back-end. 47 | /// @param nonce The nonce, provided by the Pooky back-end. 48 | /// @param proof The signature of `abi.encode(details, price, nonce, address(this))`. 49 | function boost(Boost memory details, uint256 price, bytes32 nonce, bytes calldata proof) 50 | external 51 | payable 52 | onlyVerify(abi.encode(details, price, nonce, address(this)), proof) 53 | forwarder 54 | { 55 | if (nonces.has(nonce)) { 56 | revert NonceAlreadyUsed(nonce); 57 | } 58 | 59 | nonces.set(nonce, true); 60 | 61 | if (msg.value < price) { 62 | revert InsufficientValue(price, msg.value); 63 | } 64 | 65 | emit Boosted(msg.sender, details, price); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/common/Energy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { ERC721A } from "ERC721A/ERC721A.sol"; 5 | import { ERC721ABurnable } from "ERC721A/extensions/ERC721ABurnable.sol"; 6 | import { ERC721AQueryable } from "ERC721A/extensions/ERC721AQueryable.sol"; 7 | import { IERC721A } from "ERC721A/IERC721A.sol"; 8 | import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; 9 | import { Base64 } from "solady/utils/Base64.sol"; 10 | import { LibString } from "solady/utils/LibString.sol"; 11 | import { Treasury } from "@/common/Treasury.sol"; 12 | 13 | /// @title Energy 14 | /// @author Mathieu Bour for Pooky Labs Ltd. 15 | /// Allow players to purchase a voucher representing energy on the Pooky.gg game. 16 | contract Energy is ERC721A, ERC721ABurnable, ERC721AQueryable, OwnableRoles, Treasury { 17 | using LibString for uint256; 18 | 19 | /// Role allowed to change the pricing 20 | uint256 public constant OPERATOR = _ROLE_0; 21 | 22 | /// How much cost 1 Energy point 23 | uint256 public pricing; 24 | 25 | /// How much Energy is worth each token 26 | mapping(uint256 => uint256) public values; 27 | 28 | constructor(address admin, address[] memory operators, address _treasury, uint256 _pricing) 29 | ERC721A("Pooky Energy", "PERG") 30 | Treasury(_treasury) 31 | { 32 | _initializeOwner(admin); 33 | for (uint256 i; i < operators.length;) { 34 | _grantRoles(operators[i], OPERATOR); 35 | unchecked { 36 | i++; 37 | } 38 | } 39 | 40 | pricing = _pricing; 41 | } 42 | 43 | /// @notice Change the Energy pricing 44 | function setPricing(uint256 _pricing) external onlyRolesOrOwner(OPERATOR) { 45 | pricing = _pricing; 46 | } 47 | 48 | /// @dev Voucher token IDs start at 1. 49 | function _startTokenId() internal pure override returns (uint256) { 50 | return 1; 51 | } 52 | 53 | /// @notice Generate the on-chain metadata 54 | /// @dev Expected pattern: 55 | /// ``` 56 | /// { 57 | /// "name": "Voucher {tokenId}", 58 | /// "description": "Voucher granting XX Energy on Pooky.gg", 59 | /// "attributes": [ 60 | /// { "trait_type": "Value", "value": XX } 61 | /// ] 62 | /// } 63 | /// ``` 64 | /// The function is deliberatly gas-inefficient, but far more clear. 65 | function tokenURI(uint256 tokenId) 66 | public 67 | view 68 | override(ERC721A, IERC721A) 69 | returns (string memory) 70 | { 71 | string memory value = values[tokenId].toString(); 72 | bytes memory dataURI = abi.encodePacked( 73 | "{", 74 | abi.encodePacked('"name": "Voucher #', tokenId.toString(), '",'), 75 | abi.encodePacked('"description": "Voucher granting ', value, ' Energy on Pooky.gg",'), 76 | abi.encodePacked( 77 | '"attributes": [', 78 | abi.encodePacked('{ "trait_type": "Value", "value": ', value, " }"), // Energy attribute 79 | "]" 80 | ), 81 | "}" 82 | ); 83 | 84 | return string(abi.encodePacked("data:application/json;base64,", Base64.encode(dataURI))); 85 | } 86 | 87 | /// Mint a new Energy voucher 88 | /// @param quantity The amount of Energy points 89 | /// @param recipient The address that should receive the voucher 90 | /// @dev Requirements 91 | /// - msg.value must be greater than pricing*quantity 92 | function mint(uint256 quantity, address recipient) 93 | external 94 | payable 95 | forwarder 96 | returns (uint256 tokenId) 97 | { 98 | uint256 expected = quantity * pricing; 99 | 100 | if (msg.value < expected) { 101 | revert InsufficientValue(expected, msg.value); 102 | } 103 | 104 | tokenId = _nextTokenId(); 105 | _mint(recipient, 1); 106 | values[tokenId] = quantity; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/common/IBaseERC721A.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { IERC721A } from "ERC721A/IERC721A.sol"; 5 | import { IERC721ABurnable } from "ERC721A/extensions/IERC721ABurnable.sol"; 6 | import { IERC721AQueryable } from "ERC721A/extensions/IERC721AQueryable.sol"; 7 | import { IERC165 } from "openzeppelin/interfaces/IERC165.sol"; 8 | import { IERC2981 } from "openzeppelin/interfaces/IERC2981.sol"; 9 | 10 | /// @title IBaseERC721 11 | /// 12 | /// @author Mathieu Bour for Pooky Labs Ltd. 13 | interface IBaseERC721A is IERC165, IERC721A, IERC721ABurnable, IERC721AQueryable, IERC2981 { 14 | /// Fired when the seed of a Pookyball token is set by the VRFCoordinator, 15 | event SeedSet(uint256 indexed tokenId, uint256 seed); 16 | 17 | /// Thrown when the token {tokenId} does not exist. 18 | error NonExistentToken(uint256 tokenId); 19 | 20 | // ----- ERC721A patches ----- 21 | /// @dev This allow to iterate over the token ids. 22 | function nextTokenId() external view returns (uint256); 23 | 24 | function supportsInterface(bytes4 interfaceId) 25 | external 26 | view 27 | override(IERC165, IERC721A) 28 | returns (bool); 29 | } 30 | -------------------------------------------------------------------------------- /src/common/INonceRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { IAccessControl } from "openzeppelin/access/IAccessControl.sol"; 5 | 6 | /// @title INoncesRegistry 7 | /// @author Mathieu Bour for Pooky Labs Ltd. 8 | /// 9 | /// @notice Minimal NoncesRegistry interface. 10 | interface INonceRegistry is IAccessControl { 11 | /// @notice Get the value of a given nonce. 12 | function has(bytes32 nonce) external view returns (bool); 13 | 14 | /// @notice Set the value of a given nonce. 15 | function set(bytes32 nonce, bool value) external; 16 | 17 | /// @notice Set the value of a multiple nonces. 18 | function setBatch(bytes32[] memory nonces, bool[] memory values) external; 19 | } 20 | -------------------------------------------------------------------------------- /src/common/ITreasury.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | /// @title ITreasury 5 | /// @author Mathieu Bour for Pooky Labs Ltd. 6 | interface ITreasury { 7 | /// Thrown when the msg.value of the mint function does not cover the mint cost. 8 | error InsufficientValue(uint256 expected, uint256 actual); 9 | /// Thrown when the native transfer has failed. 10 | error TransferFailed(address recipient, uint256 amount); 11 | 12 | /// Change the native currency destination address. 13 | /// @param _treasury The new treasury address. 14 | function changeTreasury(address _treasury) external; 15 | } 16 | -------------------------------------------------------------------------------- /src/common/NonceRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import { AccessControl } from "openzeppelin/access/AccessControl.sol"; 5 | import { INonceRegistry } from "@/common/INonceRegistry.sol"; 6 | 7 | /// @title NonceRegistry 8 | /// @author Mathieu Bour for Pooky Labs Ltd. 9 | /// 10 | /// @notice A nonce registry is just a mapping shared between multiple contracts. 11 | /// @dev Only OPERATOR role can set the nonces. 12 | contract NonceRegistry is INonceRegistry, AccessControl { 13 | /// The internal nonces mapping 14 | mapping(bytes32 => bool) private nonces; 15 | 16 | // Roles 17 | bytes32 public constant OPERATOR = keccak256("OPERATOR"); 18 | 19 | /// Thrown when the length of two parameters mismatch. Used in batched functions. 20 | error ArgumentSizeMismatch(uint256 x, uint256 y); 21 | 22 | constructor(address[] memory admins, address[] memory operators) { 23 | for (uint256 i = 0; i < admins.length; i++) { 24 | _grantRole(DEFAULT_ADMIN_ROLE, admins[i]); 25 | } 26 | 27 | for (uint256 i = 0; i < operators.length; i++) { 28 | _grantRole(OPERATOR, operators[i]); 29 | } 30 | } 31 | 32 | /// @notice Get the value of a given nonce. 33 | function has(bytes32 nonce) external view returns (bool) { 34 | return nonces[nonce]; 35 | } 36 | 37 | /// @notice Set the value of a given nonce. 38 | function set(bytes32 nonce, bool value) public onlyRole(OPERATOR) { 39 | nonces[nonce] = value; 40 | } 41 | 42 | /// @notice Set the value of a multiple nonces. 43 | function setBatch(bytes32[] memory _nonces, bool[] memory values) external onlyRole(OPERATOR) { 44 | if (_nonces.length != values.length) { 45 | revert ArgumentSizeMismatch(_nonces.length, values.length); 46 | } 47 | 48 | for (uint256 i = 0; i < _nonces.length; i++) { 49 | set(_nonces[i], values[i]); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/common/Rewards.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; 5 | import { ECDSA } from "solady/utils/ECDSA.sol"; 6 | import { INonceRegistry } from "@/common/INonceRegistry.sol"; 7 | import { IPookyball, PookyballMetadata, PookyballRarity } from "@/pookyball/IPookyball.sol"; 8 | import { IStickers, StickerMetadata, StickerRarity } from "@/stickers/IStickers.sol"; 9 | import { IPOK } from "@/tokens/IPOK.sol"; 10 | 11 | /// @notice Input rewards data, signed by the Pooky back-end. 12 | struct RewardsData { 13 | /// @notice The amount of native currency. 14 | uint256 amountNAT; 15 | /// @notice The amount of $POK token. 16 | uint256 amountPOK; 17 | /// @notice The rarities of the minted Pookyballs. 18 | PookyballRarity[] pookyballs; 19 | /// @notice The rarities of the minted Stickers. 20 | StickerRarity[] stickers; 21 | /// @notice The nonces that represents the payload. This prevent accounts to claim the same reward twice. 22 | bytes32[] nonces; 23 | } 24 | 25 | /// @title Rewards 26 | /// @author Mathieu Bour, Claudiu Micu for Pooky Labs Ltd. 27 | /// 28 | /// @notice Gameplay contract that allows to claim rewards native, $POK tokens and Pookyball PXP rewards. 29 | /// @dev Only authorized REWARDER-role can sign the rewards payload. 30 | contract Rewards is OwnableRoles { 31 | using ECDSA for bytes32; 32 | 33 | // Roles 34 | uint256 public constant REWARDER = _ROLE_0; 35 | 36 | // Contracts 37 | IPOK public immutable pok; 38 | IPookyball public immutable pookyball; 39 | IStickers public immutable stickers; 40 | 41 | /// To prevent users to use the same signature multiple times, we mark the rewards as claimed. 42 | INonceRegistry public nonces; 43 | 44 | /// Fired when rewards are claimed. 45 | event RewardsClaimed(address indexed account, RewardsData rewards, string data); 46 | 47 | /// Thrown when an account submits an invalid signature. 48 | error InvalidSignature(); 49 | /// Thrown when an account tries to claim rewards twice. 50 | error AlreadyClaimed(bytes32 nonce); 51 | /// Thrown when the reward contract does not own enough native currency. 52 | error InsufficientBalance(uint256 expected, uint256 actual); 53 | /// Thrown when the native transfer has failed. 54 | error TransferFailed(address recipient, uint256 amount); 55 | 56 | constructor( 57 | IPOK _pok, 58 | IPookyball _pookyball, 59 | IStickers _stickers, 60 | INonceRegistry _nonces, 61 | address admin, 62 | address[] memory rewarders 63 | ) { 64 | pok = _pok; 65 | pookyball = _pookyball; 66 | stickers = _stickers; 67 | nonces = _nonces; 68 | 69 | // Set up the roles 70 | _initializeOwner(admin); 71 | uint256 length = rewarders.length; 72 | for (uint256 i; i < length;) { 73 | _grantRoles(rewarders[i], REWARDER); 74 | unchecked { 75 | i++; 76 | } 77 | } 78 | } 79 | 80 | /// @notice Receive funds that will be used for native token reward. 81 | receive() external payable { } 82 | 83 | /// @notice Recover all the funds on the contract. 84 | function withdraw() external onlyOwner { 85 | (bool sent,) = address(msg.sender).call{ value: address(this).balance }(""); 86 | if (!sent) { 87 | revert TransferFailed(msg.sender, address(this).balance); 88 | } 89 | } 90 | 91 | /// @notice Claim rewards using a signature generated from the Pooky game back-end. 92 | /// Rewards include: native currency, $POK tokens, PXP for the Pookyball tokens and Pookyball tokens. 93 | /// @dev Requirements: 94 | /// - signature is valid 95 | /// - tokenIds and tokenPXP have the same size 96 | /// - contract has enough native currency to reward player 97 | function claim(RewardsData memory rewards, bytes memory signature, string memory data) external { 98 | // Generate the signed message from the sender, rewards and nonce 99 | bytes32 hash = keccak256(abi.encode(msg.sender, rewards, data)).toEthSignedMessageHash(); 100 | 101 | // Ensure that all rewards are claimed only once 102 | uint256 length = rewards.nonces.length; 103 | for (uint256 i; i < length; i++) { 104 | if (nonces.has(rewards.nonces[i])) { 105 | revert AlreadyClaimed(rewards.nonces[i]); 106 | } 107 | 108 | nonces.set(rewards.nonces[i], true); 109 | } 110 | 111 | if (!hasAllRoles(hash.recover(signature), REWARDER)) { 112 | revert InvalidSignature(); 113 | } 114 | 115 | // Mint $POK token 116 | if (rewards.amountPOK > 0) { 117 | pok.mint(msg.sender, rewards.amountPOK); 118 | } 119 | 120 | // Mint Pookyballs 121 | if (rewards.pookyballs.length > 0) { 122 | uint256 pookyballCount = rewards.pookyballs.length; 123 | address[] memory addresses = new address[](pookyballCount); 124 | 125 | for (uint256 i; i < pookyballCount;) { 126 | addresses[i] = msg.sender; 127 | unchecked { 128 | i++; 129 | } 130 | } 131 | 132 | pookyball.mint(addresses, rewards.pookyballs); 133 | } 134 | 135 | // Mint Stickers 136 | if (rewards.stickers.length > 0) { 137 | stickers.mint(msg.sender, rewards.stickers); 138 | } 139 | 140 | // Transfer native currency 141 | if (rewards.amountNAT > 0) { 142 | // Ensure that the contract has enough tokens to deliver 143 | if (address(this).balance < rewards.amountNAT) { 144 | revert InsufficientBalance(rewards.amountNAT, address(this).balance); 145 | } 146 | 147 | (bool sent,) = address(msg.sender).call{ value: rewards.amountNAT }(""); 148 | if (!sent) { 149 | revert TransferFailed(msg.sender, rewards.amountNAT); 150 | } 151 | } 152 | 153 | emit RewardsClaimed(msg.sender, rewards, data); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/common/Signer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; 5 | import { ECDSA } from "solady/utils/ECDSA.sol"; 6 | 7 | /// @title Signer 8 | /// @author Mathieu Bour for Pooky Labs Ltd. 9 | /// 10 | /// @dev Provide the `verify` function and the `onlyVerify` modifier to child contracts. 11 | abstract contract Signer is OwnableRoles { 12 | using ECDSA for bytes32; 13 | 14 | uint256 public constant SIGNER = _ROLE_42; 15 | 16 | /// Thrown when the signature is invalid. 17 | error InvalidSignature(); 18 | 19 | constructor(address signer) { 20 | _grantRoles(signer, SIGNER); 21 | } 22 | 23 | /// @notice Ensure that `data` has been signed by a `SIGNER` using the `proof`. 24 | function verify(bytes memory data, bytes calldata proof) internal view { 25 | // Generate the signed message from the tokenId and currentPXP 26 | bytes32 hash = keccak256(data).toEthSignedMessageHash(); 27 | 28 | if (!hasAllRoles(hash.recoverCalldata(proof), SIGNER)) { 29 | revert InvalidSignature(); 30 | } 31 | } 32 | 33 | /// @notice Modifier version of the `verify` function. 34 | modifier onlyVerify(bytes memory data, bytes calldata proof) { 35 | verify(data, proof); 36 | _; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/common/Treasury.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { Ownable } from "solady/auth/Ownable.sol"; 5 | import { ITreasury } from "@/common/ITreasury.sol"; 6 | 7 | /// @title Treasury 8 | /// @author Mathieu Bour for Pooky Labs Ltd. 9 | /// 10 | /// @notice Base class for contracts that are made to receive native currency. 11 | /// The destination address is controller by the contract owner. 12 | abstract contract Treasury is Ownable, ITreasury { 13 | /// The native currency destination address. 14 | address public treasury; 15 | 16 | constructor(address _treasury) { 17 | treasury = _treasury; 18 | } 19 | 20 | /// @notice Forward the funds to the treasury wallet at the end of the transaction. 21 | /// Since `treasury` is a trusted address, this modifier should not lead to any re-entrancy issue. 22 | modifier forwarder() { 23 | _; 24 | 25 | uint256 value = address(this).balance; 26 | (bool sent,) = treasury.call{ value: value }(""); 27 | if (!sent) { 28 | revert TransferFailed(treasury, value); 29 | } 30 | } 31 | 32 | /// Change the native currency destination address. 33 | /// @param _treasury The new treasury address. 34 | function changeTreasury(address _treasury) external onlyOwner { 35 | treasury = _treasury; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pookyball/IPookyball.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import { IAccessControl } from "openzeppelin/access/IAccessControl.sol"; 5 | import { IERC721 } from "openzeppelin/token/ERC721/IERC721.sol"; 6 | import { IERC2981 } from "openzeppelin/interfaces/IERC2981.sol"; 7 | 8 | /// @title PookyballMetadata 9 | /// @notice The Pookyball rarities are represented on chain by this enum. 10 | enum PookyballRarity { 11 | COMMON, 12 | RARE, 13 | EPIC, 14 | LEGENDARY, 15 | MYTHIC 16 | } 17 | 18 | /// @title PookyballMetadata 19 | /// @notice Pookyballs NFT have the following features: 20 | /// - rarity: integer enum. 21 | /// - level: token level, can be increase by spending token experiences points (PXP). 22 | /// - pxp: token experience points. 23 | /// - seed: a random uint256 word provided by Chainlink VRF service that will be used by Pooky's NFT generator 24 | /// back-end to generate the NFT visuals and in-game statistics\. 25 | struct PookyballMetadata { 26 | PookyballRarity rarity; 27 | uint256 level; 28 | uint256 pxp; 29 | uint256 seed; 30 | } 31 | 32 | /// @title IPookyball 33 | /// @author Mathieu Bour for Pooky Labs Ltd. 34 | /// @notice Minimal Pookyball interface. 35 | interface IPookyball is IAccessControl, IERC2981, IERC721 { 36 | /// Fired when the seed of a Pookyball token is set by the VRFCoordinator 37 | event SeedSet(uint256 indexed tokenId, uint256 seed); 38 | /// Fired when the level of a Pookyball token is changed 39 | event LevelChanged(uint256 indexed tokenId, uint256 level); 40 | /// Fired when the PXP of a Pookyball token is changed 41 | event PXPChanged(uint256 indexed tokenId, uint256 amount); 42 | 43 | /// Thrown when the length of two parameters mismatch. Used in the mint batched function. 44 | error ArgumentSizeMismatch(uint256 x, uint256 y); 45 | 46 | /// @notice PookyballMetadata of the token {tokenId}. 47 | /// @dev Requirements: 48 | /// - Pookyball {tokenId} should exist (minted and not burned). 49 | function metadata(uint256 tokenId) external view returns (PookyballMetadata memory); 50 | 51 | /// @notice Change the secondary sale royalties receiver address. 52 | function setERC2981Receiver(address newReceiver) external; 53 | 54 | /// @notice Mint a new Pookyball token with a given rarity. 55 | function mint(address[] memory recipients, PookyballRarity[] memory rarities) 56 | external 57 | returns (uint256); 58 | 59 | /// @notice Change the level of a Pookyball token. 60 | /// @dev Requirements: 61 | /// - Pookyball {tokenId} should exist (minted and not burned). 62 | function setLevel(uint256 tokenId, uint256 newLevel) external; 63 | 64 | /// @notice Change the PXP of a Pookyball token. 65 | /// @dev Requirements: 66 | /// - Pookyball {tokenId} should exist (minted and not burned). 67 | function setPXP(uint256 tokenId, uint256 newPXP) external; 68 | } 69 | -------------------------------------------------------------------------------- /src/pookyball/PookyballLevelUp.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.22; 3 | 4 | import { LevelUp } from "@/common/LevelUp.sol"; 5 | import { IPookyball, PookyballMetadata, PookyballRarity } from "@/pookyball/IPookyball.sol"; 6 | import { IPOK } from "@/tokens/IPOK.sol"; 7 | 8 | /// @title PookyballLevelUp 9 | /// @author Mathieu Bour for Pooky Labs Ltd. 10 | /// 11 | /// @notice Allow to level up Pooky Pookyball tokens. 12 | contract PookyballLevelUp is LevelUp { 13 | /// The Pookyball contract. 14 | IPookyball pookyball; 15 | 16 | /// @param _pookyball The Pookyball ERC-721 contract. 17 | /// @param _pok The POK ERC-20 contract. 18 | /// @param admin The new admin address. 19 | /// @param _signer The inital signer address. 20 | /// @param _treasury The initial treasury address. 21 | constructor(IPookyball _pookyball, IPOK _pok, address admin, address _signer, address _treasury) 22 | LevelUp(_pok, admin, _signer, _treasury, 60e18, 120) 23 | { 24 | pookyball = _pookyball; 25 | } 26 | 27 | /// @notice Get the levelling parameters for a given token. 28 | /// @param tokenId The token id. 29 | /// @return currentLevel The current token level. 30 | /// @return maxLevel The maximum allowed level. 31 | function getParams(uint256 tokenId) 32 | public 33 | view 34 | override 35 | returns (uint256 currentLevel, uint256 maxLevel) 36 | { 37 | PookyballMetadata memory metadata = pookyball.metadata(tokenId); 38 | currentLevel = metadata.level; 39 | 40 | if (metadata.rarity == PookyballRarity.COMMON) { 41 | maxLevel = 40; 42 | } else if (metadata.rarity == PookyballRarity.RARE) { 43 | maxLevel = 60; 44 | } else if (metadata.rarity == PookyballRarity.EPIC) { 45 | maxLevel = 80; 46 | } else if (metadata.rarity == PookyballRarity.LEGENDARY) { 47 | maxLevel = 100; 48 | } else if (metadata.rarity == PookyballRarity.MYTHIC) { 49 | maxLevel = 120; 50 | } 51 | } 52 | 53 | function _apply(uint256 tokenId, uint256 newLevel, uint256) internal virtual override { 54 | pookyball.setLevel(tokenId, newLevel); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/pookyball/PookyballReroll.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; 5 | import { INonceRegistry } from "@/common/INonceRegistry.sol"; 6 | import { Signer } from "@/common/Signer.sol"; 7 | import { Treasury } from "@/common/Treasury.sol"; 8 | import { IPookyball } from "@/pookyball/IPookyball.sol"; 9 | 10 | /// @title PookyballReroll 11 | /// @author Mathieu Bour for Pooky Labs Ltd. 12 | /// Allow player to reroll their Pookyball attributes. 13 | contract PookyballReroll is OwnableRoles, Signer, Treasury { 14 | /// Fired when a Pookyball stats have been rerolled. 15 | event Reroll(uint256 indexed tokenId, uint256 price); 16 | 17 | /// Thrown when user attempts to use a nonce that was already used in the past. 18 | error NonceAlreadyUsed(bytes32 nonce); 19 | /// Thrown when user attempts to reroll a Pookyball he does not own. 20 | error OwnershipRequired(uint256 tokenId); 21 | 22 | /// The Pookyball contract. 23 | IPookyball public immutable pookyball; 24 | /// The NonceRegistry contract. 25 | INonceRegistry public immutable nonces; 26 | 27 | constructor( 28 | IPookyball _pookyball, 29 | INonceRegistry _nonces, 30 | address admin, 31 | address _signer, 32 | address _treasury 33 | ) Signer(_signer) Treasury(_treasury) { 34 | _initializeOwner(admin); 35 | pookyball = _pookyball; 36 | nonces = _nonces; 37 | } 38 | 39 | /// @notice Reroll the attributes of an existing Pookyball. 40 | /// @dev Pricing is controlled by the backend, which need to provide a proof to the end user. 41 | /// @param tokenId The Pookyball token id to reroll. 42 | /// @param price The price in native currency, provided by the Pooky back-end. 43 | /// @param nonce The nonce, provided by the Pooky back-end. 44 | /// @param proof The signature of `abi.encode(tokenId, price, nonce, address(this))`. 45 | function reroll(uint256 tokenId, uint256 price, bytes32 nonce, bytes calldata proof) 46 | external 47 | payable 48 | onlyVerify(abi.encode(tokenId, price, nonce, address(this)), proof) 49 | forwarder 50 | { 51 | if (nonces.has(nonce)) { 52 | revert NonceAlreadyUsed(nonce); 53 | } 54 | nonces.set(nonce, true); 55 | 56 | if (pookyball.ownerOf(tokenId) != msg.sender) { 57 | revert OwnershipRequired(tokenId); 58 | } 59 | 60 | if (msg.value < price) { 61 | revert InsufficientValue(price, msg.value); 62 | } 63 | 64 | emit Reroll(tokenId, price); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/pookyball/Pressure.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import { IPOK } from "@/tokens/IPOK.sol"; 5 | 6 | /// @title Pressure 7 | /// @author Mathieu Bour for Pooky Labs Ltd. 8 | /// 9 | /// @notice Gameplay contract that allow to inflate/repair Pookyball tokens by spending $POK tokens and native currency. 10 | /// @dev This contract has the POK.BURNER role. 11 | contract Pressure { 12 | // Constants 13 | uint8[] public floors = [10, 20, 30, 40, 50, 60, 75, 100]; 14 | uint256[] public floorsNAT = 15 | [0.064e18, 0.0538e18, 0.0452e18, 0.0379e18, 0.0319e18, 0.0268e18, 0.0225e18, 0.0189e18]; 16 | uint256[] public floorsPOK = 17 | [2.286e18, 1.92e18, 1.613e18, 1.355e18, 1.138e18, 0.956e18, 0.803e18, 0.674e18]; 18 | 19 | // Contracts 20 | IPOK immutable pok; 21 | address immutable treasury; 22 | 23 | /// Emitted when the Pookyball has been inflated. 24 | event Inflated(uint256 indexed tokenId, uint8 current, uint8 amount); 25 | 26 | /// Thrown when the inflate parameters greater than 100 27 | error InvalidParameters(uint256 current, uint256 amount); 28 | /// Thrown when the msg.value of the inflate function does not cover the inflate cost. 29 | error InsufficientValue(uint256 expected, uint256 actual); 30 | /// Thrown when the sender does own enough $POK tokens. 31 | error InsufficientPOK(uint256 expected, uint256 actual); 32 | /// Thrown when the native transfer has failed. 33 | error TransferFailed(address recipient, uint256 amount); 34 | 35 | constructor(IPOK _pok, address _treasury) { 36 | pok = _pok; 37 | treasury = _treasury; 38 | } 39 | 40 | /// @notice Compute the cost using the floors. 41 | /// @param current The current token pressure. 42 | /// @param amount The desired pressure increase. 43 | /// @param values The floor values. 44 | function compute(uint8 current, uint8 amount, uint256[] memory values) 45 | internal 46 | view 47 | returns (uint256) 48 | { 49 | if (current + amount > 100) { 50 | revert InvalidParameters(current, amount); 51 | } 52 | 53 | uint256 sum = 0; 54 | 55 | for (uint256 i = 0; i < floors.length; i++) { 56 | if (amount == 0) break; 57 | if (current > floors[i]) continue; 58 | 59 | uint8 size = floors[i] + 1 - current; 60 | uint8 delta = size > amount ? amount : size; 61 | 62 | sum += values[i] * delta; 63 | current += delta; 64 | amount -= delta; 65 | } 66 | 67 | return sum; 68 | } 69 | 70 | /// @notice Get the price to inflate a Pookyball token in native currency. 71 | /// @param current The current token pressure. 72 | /// @param amount The desired pressure increase. 73 | function priceNAT(uint8 current, uint8 amount) public view returns (uint256) { 74 | return compute(current, amount, floorsNAT); 75 | } 76 | 77 | /// @notice Get the price to inflate a Pookyball token in $POK tokens. 78 | /// @param current The current token pressure. 79 | /// @param amount The desired pressure increase. 80 | function pricePOK(uint8 current, uint8 amount) public view returns (uint256) { 81 | return compute(current, amount, floorsPOK); 82 | } 83 | 84 | /// @notice Compute the cost using the floors. 85 | /// @param tokenId The Pookyball token id to inflate. 86 | /// @param current The current token pressure. 87 | /// @param amount The desired pressure increase. 88 | function inflate(uint256 tokenId, uint8 current, uint8 amount) external payable { 89 | if (msg.value > 0) { 90 | // Sender is paying with native currency 91 | uint256 amountNAT = priceNAT(current, amount); 92 | 93 | if (msg.value < amountNAT) { 94 | revert InsufficientValue(msg.value, amountNAT); 95 | } 96 | 97 | (bool sent,) = address(treasury).call{ value: amountNAT }(""); 98 | if (!sent) { 99 | revert TransferFailed(msg.sender, amountNAT); 100 | } 101 | } else { 102 | // Sender is paying with $POK tokens 103 | uint256 amountPOK = pricePOK(current, amount); 104 | 105 | if (pok.balanceOf(msg.sender) < amountPOK) { 106 | revert InsufficientPOK(pok.balanceOf(msg.sender), amountPOK); 107 | } 108 | 109 | pok.burn(msg.sender, amountPOK); 110 | } 111 | 112 | emit Inflated(tokenId, current, amount); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/stickers/IStickers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { IBaseERC721A } from "@/common/IBaseERC721A.sol"; 5 | 6 | enum StickerRarity { 7 | COMMON, 8 | RARE, 9 | EPIC, 10 | LEGENDARY, 11 | MYTHIC 12 | } 13 | 14 | struct StickerMint { 15 | address recipient; 16 | StickerRarity rarity; 17 | } 18 | 19 | struct StickerMetadata { 20 | uint248 level; 21 | StickerRarity rarity; 22 | } 23 | 24 | /// @title IStickers 25 | /// @author Mathieu Bour for Pooky Labs Ltd. 26 | interface IStickers is IBaseERC721A { 27 | /// Fired when the level of a Pookyball token is changed, 28 | event LevelChanged(uint256 indexed tokenId, uint256 level); 29 | 30 | /// @notice StickerMetadata of the token {tokenId}. 31 | /// @dev Requirements: 32 | /// - Sticker {tokenId} should exist (minted and not burned). 33 | function metadata(uint256 tokenId) external view returns (StickerMetadata memory); 34 | 35 | /// @notice Change the level of a Sticker token. 36 | /// @dev Requirements: 37 | /// - Sticker {tokenId} should exist (minted and not burned). 38 | function setLevel(uint256 tokenId, uint248 newLevel) external; 39 | 40 | /// @notice Mint multiple Stickers at once. 41 | /// @param recipient The mint recipient. 42 | /// @param rarities The Sticker rarities. 43 | function mint(address recipient, StickerRarity[] memory rarities) external; 44 | } 45 | -------------------------------------------------------------------------------- /src/stickers/IStickersController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { IPookyball } from "@/pookyball/IPookyball.sol"; 5 | import { IStickers } from "@/stickers/IStickers.sol"; 6 | 7 | /// @notice IStickersController 8 | /// @author Mathieu Bour for Pooky Labs Ltd. 9 | interface IStickersController { 10 | /// @notice Fired when a sticker is attached to a Pookyball. 11 | event StickerAttached(uint256 stickerId, uint256 pookyballId); 12 | /// @notice Fired when a sticker is replace from a Pookyball. 13 | event StickerReplaced(uint256 stickerId, uint256 previousStickerId, uint256 pookyballId); 14 | /// @notice Fired when a sticker is detached from a Pookyball. 15 | event StickerDetached(uint256 stickerId, uint256 pookyballId); 16 | 17 | /// @notice Thrown when a sticker is invalid. 18 | error InvalidSticker(uint256 stickerId); 19 | 20 | /// @notice The Stickers ERC-721 contract. 21 | function stickers() external view returns (IStickers); 22 | 23 | /// @notice The Pookyball ERC-721 contract. 24 | function pookyball() external view returns (IPookyball); 25 | 26 | /// @notice Get the Pookyball token id linked to a Sticker. 27 | /// @param stickerId The Sticker token id. 28 | function attachedTo(uint256 stickerId) external view returns (uint256); 29 | 30 | /// @notice Get the Stickers token ids attached to a Pookyball. 31 | /// @param pookyballId The Pookyball token id. 32 | function slots(uint256 pookyballId) external view returns (uint256[] memory); 33 | 34 | /// @notice Attach a sticker to a Pookyball. 35 | /// @param stickerId The sticker token id. 36 | /// @param pookyballId The Pookyball token id. 37 | /// @dev Caution: no ownership checks are run. 38 | function attach(uint256 stickerId, uint256 pookyballId) external; 39 | 40 | /// @notice Replace a sticker from a Pookyball, burning the previous one. 41 | /// @param stickerId The sticker token id. 42 | /// @param previousStickerId The previous sticker token id that will be burned. 43 | /// @param pookyballId The Pookyball token id. 44 | /// @dev Caution: no ownership checks are run. 45 | function replace(uint256 stickerId, uint256 previousStickerId, uint256 pookyballId) external; 46 | 47 | /// @notice Detach (remove) a sticker from a Pookyball. 48 | /// @param stickerId The Sstickerticker token id. 49 | /// @param recepient The address when to send the detached sticker. 50 | function detach(uint256 stickerId, address recepient) external; 51 | } 52 | -------------------------------------------------------------------------------- /src/stickers/Stickers.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { ERC721A } from "ERC721A/ERC721A.sol"; 5 | import { IERC721A } from "ERC721A/IERC721A.sol"; 6 | import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; 7 | import { BaseERC721A } from "@/common/BaseERC721A.sol"; 8 | import { IBaseERC721A } from "@/common/IBaseERC721A.sol"; 9 | import { IStickers, StickerMetadata, StickerRarity, StickerMint } from "@/stickers/IStickers.sol"; 10 | import { VRFConfig } from "@/types/VRFConfig.sol"; 11 | 12 | /// @title Stickers 13 | /// @author Mathieu Bour for Pooky Labs Ltd. 14 | /// @dev Implemented roles: 15 | /// - Owner: allowed to change metadata of the Stickers 16 | /// - `MINTER`: allowed to mint new Stickers 17 | /// - `GAME`: allowed to change the game attributes of the Stickers 18 | contract Stickers is IStickers, BaseERC721A, OwnableRoles { 19 | uint248 constant DEFAULT_LEVEL = 0; 20 | 21 | // Roles 22 | uint256 public constant MINTER = _ROLE_0; 23 | uint256 public constant GAME = _ROLE_1; 24 | 25 | /// @notice Tokens gameplay metadata, see `StickerMetadata` 26 | mapping(uint256 => StickerMetadata) _metadata; 27 | 28 | constructor(address admin, address _receiver, VRFConfig memory _vrf) 29 | BaseERC721A( 30 | admin, 31 | "Pooky Stickers", 32 | "STK", 33 | "https://metadata.pooky.gg/stickers/", 34 | "https://metadata.pooky.gg/contracts/Stickers.json", 35 | _vrf, 36 | _receiver, 37 | 500 // 5% royalties by default 38 | ) 39 | { } 40 | 41 | /// @notice Get the StickerMetadata of the token `tokenId`. 42 | /// @dev Requirements: 43 | /// - Sticker `tokenId` should exist (minted and not burned). 44 | function metadata(uint256 tokenId) 45 | external 46 | view 47 | onlyExists(tokenId) 48 | returns (StickerMetadata memory) 49 | { 50 | return _metadata[tokenId]; 51 | } 52 | 53 | /// @notice Change the level of a Sticker token. 54 | /// @dev Requirements: 55 | /// - sender must have the `GAME` role or be the owner. 56 | /// - Sticker `tokenId` should exist (minted and not burned). 57 | function setLevel(uint256 tokenId, uint248 newLevel) 58 | external 59 | onlyExists(tokenId) 60 | onlyRolesOrOwner(GAME) 61 | { 62 | _metadata[tokenId].level = newLevel; 63 | emit LevelChanged(tokenId, newLevel); 64 | } 65 | 66 | /// @notice Mint a new Sticker token with a given rarity. 67 | /// Level and seed are set to zero, entropy is requested to the VRF coordinator. 68 | /// @dev Requirements: 69 | /// - sender must have the `MINTER` role or be the owner. 70 | function mint(address recipient, StickerRarity[] memory rarities) 71 | external 72 | onlyRolesOrOwner(MINTER) 73 | { 74 | uint256 quantity = rarities.length; 75 | uint256 start = _nextTokenId(); 76 | 77 | _mint(recipient, quantity); 78 | 79 | for (uint256 i = 0; i < quantity;) { 80 | _metadata[start + i] = StickerMetadata( 81 | DEFAULT_LEVEL, // Stickers are level 0 by default 82 | rarities[i] 83 | ); 84 | 85 | unchecked { 86 | i++; 87 | } 88 | } 89 | } 90 | 91 | // ----- ERC165 ----- 92 | /// @notice IERC165 declaration. 93 | /// @dev Supports the following `interfaceId`s: 94 | /// - IERC165: 0x01ffc9a7 95 | /// - IERC721: 0x80ac58cd 96 | /// - IERC721Metadata: 0x5b5e139f 97 | /// - IERC2981: 0x2a55205a 98 | function supportsInterface(bytes4 interfaceId) 99 | public 100 | view 101 | virtual 102 | override(BaseERC721A, IBaseERC721A) 103 | returns (bool) 104 | { 105 | return super.supportsInterface(interfaceId); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/stickers/StickersController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { EnumerableSet } from "openzeppelin/utils/structs/EnumerableSet.sol"; 5 | import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; 6 | import { IPookyball } from "@/pookyball/IPookyball.sol"; 7 | import { IStickers } from "@/stickers/IStickers.sol"; 8 | import { IStickersController } from "@/stickers/IStickersController.sol"; 9 | 10 | /// @title StickersController 11 | /// @author Mathieu Bour for Pooky Labs Ltd. 12 | /// 13 | /// @notice This contract contains the relationship data between the Pookyballs and the Stickers. 14 | /// @dev This contract only handles the Pookyball <=> Sticker association and does not run any check. 15 | /// It also have the {Stickers-OPERATOR} role, allowing to freely move the Stickers tokens. 16 | /// When a Sticker is linked to a Pookyball, the Sticker is transfered to this contract so the original 17 | /// owner cannot sell the token anymore on any platform. 18 | contract StickersController is IStickersController, OwnableRoles { 19 | using EnumerableSet for EnumerableSet.UintSet; 20 | 21 | // Roles 22 | uint256 public constant LINKER = _ROLE_0; 23 | uint256 public constant REPLACER = _ROLE_1; 24 | uint256 public constant REMOVER = _ROLE_2; 25 | 26 | mapping(uint256 => uint256) private _links; 27 | mapping(uint256 => EnumerableSet.UintSet) private _slots; 28 | 29 | IPookyball public immutable pookyball; 30 | IStickers public immutable stickers; 31 | 32 | constructor(IPookyball _pookyball, IStickers _stickers, address admin) { 33 | pookyball = _pookyball; 34 | stickers = _stickers; 35 | 36 | _initializeOwner(admin); 37 | } 38 | 39 | /// @notice Get the Pookyball token id linked to a Sticker. 40 | /// @param stickerId The Sticker token id. 41 | function attachedTo(uint256 stickerId) external view returns (uint256) { 42 | return _links[stickerId]; 43 | } 44 | 45 | /// @notice Get the Sticker token ids attached to a Pookyball. 46 | /// @param pookyballId The Pookyball token id. 47 | function slots(uint256 pookyballId) external view returns (uint256[] memory) { 48 | return _slots[pookyballId].values(); 49 | } 50 | 51 | /// @notice Attach a Sticker to a Pookyball (internal). 52 | /// @param stickerId The Sticker token id. 53 | /// @param pookyballId The Pookyball token id. 54 | /// @dev Caution: no role/ownership checks are run. 55 | function _attach(uint256 stickerId, uint256 pookyballId) internal { 56 | _slots[pookyballId].add(stickerId); 57 | _links[stickerId] = pookyballId; 58 | stickers.transferFrom(stickers.ownerOf(stickerId), address(this), stickerId); 59 | } 60 | 61 | /// @notice Attach a Sticker to a Pookyball. 62 | /// @param stickerId The Sticker token id. 63 | /// @param pookyballId The Pookyball token id. 64 | /// @dev Caution: no token ownership checks are run. 65 | function attach(uint256 stickerId, uint256 pookyballId) public onlyRolesOrOwner(LINKER) { 66 | _attach(stickerId, pookyballId); 67 | emit StickerAttached(stickerId, pookyballId); 68 | } 69 | 70 | /// @notice Replace a sticker from a Pookyball, burning the previous one. 71 | /// @param stickerId The Sticker token id. 72 | /// @param previousStickerId The previous Sticker token id that will be burned. 73 | /// @param pookyballId The Pookyball token id. 74 | /// @dev Caution: no token ownership checks are run. 75 | function replace(uint256 stickerId, uint256 previousStickerId, uint256 pookyballId) 76 | public 77 | onlyRolesOrOwner(REPLACER) 78 | { 79 | if (!_slots[pookyballId].contains(previousStickerId)) { 80 | revert InvalidSticker(previousStickerId); 81 | } 82 | 83 | _slots[pookyballId].remove(previousStickerId); 84 | delete _links[previousStickerId]; 85 | stickers.burn(previousStickerId); 86 | _attach(stickerId, pookyballId); 87 | emit StickerReplaced(stickerId, previousStickerId, pookyballId); 88 | } 89 | 90 | /// @notice Detach (remove) a Sticker from a Pookyball. 91 | /// @param stickerId The Sticker token id. 92 | /// @param recepient The address when to send the detached Sticker. 93 | /// @dev Caution: no token ownership checks are run. 94 | function detach(uint256 stickerId, address recepient) external onlyRolesOrOwner(REMOVER) { 95 | uint256 pookyballId = _links[stickerId]; 96 | _slots[pookyballId].remove(stickerId); 97 | delete _links[stickerId]; 98 | stickers.transferFrom(address(this), recepient, stickerId); 99 | emit StickerDetached(stickerId, pookyballId); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/stickers/StickersLevelUp.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { LevelUp } from "@/common/LevelUp.sol"; 5 | import { IStickers, StickerMetadata, StickerRarity } from "@/stickers/IStickers.sol"; 6 | import { IPOK } from "@/tokens/IPOK.sol"; 7 | 8 | /// @title StickersLevelUp 9 | /// @author Mathieu Bour for Pooky Labs Ltd. 10 | /// 11 | /// @notice Allow to level up Pooky Stickers tokens. 12 | contract StickersLevelUp is LevelUp { 13 | /// The Stickers contract. 14 | IStickers stickers; 15 | 16 | constructor(IStickers _stickers, IPOK _pok, address admin, address _signer, address _treasury) 17 | LevelUp(_pok, admin, _signer, _treasury, 20e18, 120) 18 | { 19 | stickers = _stickers; 20 | } 21 | 22 | /// @notice Get the levelling parameters for a given token. 23 | /// @param tokenId The token id. 24 | /// @return currentLevel The current token level. 25 | /// @return maxLevel The maximum allowed level. 26 | function getParams(uint256 tokenId) public view override returns (uint256, uint256 maxLevel) { 27 | StickerMetadata memory metadata = stickers.metadata(tokenId); 28 | 29 | if (metadata.rarity == StickerRarity.COMMON) { 30 | maxLevel = 40; 31 | } else if (metadata.rarity == StickerRarity.RARE) { 32 | maxLevel = 60; 33 | } else if (metadata.rarity == StickerRarity.EPIC) { 34 | maxLevel = 80; 35 | } else if (metadata.rarity == StickerRarity.LEGENDARY) { 36 | maxLevel = 100; 37 | } else if (metadata.rarity == StickerRarity.MYTHIC) { 38 | maxLevel = 120; 39 | } 40 | 41 | return (uint256(metadata.level), maxLevel); 42 | } 43 | 44 | function _apply(uint256 tokenId, uint256 newLevel, uint256) internal virtual override { 45 | stickers.setLevel(tokenId, uint248(newLevel)); // Safe cast, restricted by maxLevel above 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/stickers/StickersManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { IPookyball, PookyballMetadata, PookyballRarity } from "@/pookyball/IPookyball.sol"; 5 | import { IStickers } from "@/stickers/IStickers.sol"; 6 | import { IStickersController } from "@/stickers/IStickersController.sol"; 7 | 8 | /// @title StickersManager 9 | /// @author Mathieu Bour for Pooky Labs Ltd. 10 | /// 11 | /// @dev Implementation of the manager that allows end users to attach or replace stickers to Pookyballs. 12 | contract StickersManager { 13 | IStickers public immutable stickers; 14 | IPookyball public immutable pookyball; 15 | IStickersController public immutable controller; 16 | 17 | error OwnershipRequired(address token, uint256 tokenId); 18 | error InsufficientFreeSlot(uint256 pookyballId); 19 | 20 | constructor(IStickersController _controller) { 21 | controller = _controller; 22 | stickers = _controller.stickers(); 23 | pookyball = _controller.pookyball(); 24 | } 25 | 26 | modifier checkOwnership(uint256 stickerId, uint256 pookyballId) { 27 | if (stickers.ownerOf(stickerId) != msg.sender) { 28 | revert OwnershipRequired(address(stickers), stickerId); 29 | } 30 | 31 | if (pookyball.ownerOf(pookyballId) != msg.sender) { 32 | revert OwnershipRequired(address(pookyball), pookyballId); 33 | } 34 | 35 | _; 36 | } 37 | 38 | function slots(uint256 pookyballId) 39 | public 40 | view 41 | returns (uint256 total, uint256 unlocked, uint256 free) 42 | { 43 | PookyballMetadata memory metadata = pookyball.metadata(pookyballId); 44 | 45 | if (metadata.rarity == PookyballRarity.COMMON) { 46 | total = 4; 47 | } else if (metadata.rarity == PookyballRarity.RARE) { 48 | total = 6; 49 | } else if (metadata.rarity == PookyballRarity.EPIC) { 50 | total = 8; 51 | } else if (metadata.rarity == PookyballRarity.LEGENDARY) { 52 | total = 10; 53 | } else if (metadata.rarity == PookyballRarity.MYTHIC) { 54 | total = 12; 55 | } 56 | 57 | unlocked = (metadata.level + 5) / 10; 58 | uint256 used = controller.slots(pookyballId).length; 59 | 60 | // We might have some promotional offers that allow to unlock the slots before the Pookyball has reached the required level 61 | if (used > unlocked) { 62 | unlocked = used; 63 | } 64 | 65 | free = unlocked - used; 66 | } 67 | 68 | function attach(uint256 stickerId, uint256 pookyballId) 69 | external 70 | checkOwnership(stickerId, pookyballId) 71 | { 72 | (,, uint256 free) = slots(pookyballId); 73 | 74 | if (free == 0) { 75 | revert InsufficientFreeSlot(pookyballId); 76 | } 77 | 78 | controller.attach(stickerId, pookyballId); 79 | } 80 | 81 | function replace(uint256 stickerId, uint256 previousStickerId, uint256 pookyballId) 82 | external 83 | checkOwnership(stickerId, pookyballId) 84 | { 85 | controller.replace(stickerId, previousStickerId, pookyballId); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/stickers/StickersSale.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { OwnableRoles } from "solady/auth/OwnableRoles.sol"; 5 | import { Treasury } from "@/common/Treasury.sol"; 6 | import { IStickers, StickerMint, StickerRarity } from "@/stickers/IStickers.sol"; 7 | 8 | struct Pack { 9 | uint256 price; 10 | /// Supply of the current sale 11 | uint256 supply; 12 | /// Total number of tokens minted during all the sales 13 | uint256 minted; 14 | /// Total supply of all the sales 15 | uint256 totalSupply; 16 | PackContent content; 17 | } 18 | 19 | struct PackContent { 20 | /// How many common stickers are in the pack. 21 | uint256 common; 22 | /// How many rare stickers are in the pack. 23 | uint256 rare; 24 | /// How many epic stickers are in the pack. 25 | uint256 epic; 26 | /// How many legendary stickers are in the pack. 27 | uint256 legendary; 28 | } 29 | 30 | struct Refill { 31 | uint256 packId; 32 | /// The token price in native currency wei. 33 | uint256 price; 34 | /// The refill token quantity. 35 | uint256 quantity; 36 | } 37 | 38 | /// @title StickersSale 39 | /// @author Mathieu Bour for Pooky Labs Ltd. 40 | /// @notice Sticker Sale contract that can be refilled by an admin. 41 | /// @dev Roles: 42 | /// - Owner: allowd to create new packs 43 | /// - Seller: allowed to refill the sale 44 | contract StickersSale is OwnableRoles, Treasury { 45 | uint256 public constant SELLER = _ROLE_0; 46 | 47 | /// The Stickers contract address. 48 | IStickers public immutable stickers; 49 | 50 | /// The date when next mint window will open (compared to block.timestamp). 51 | uint256 public closedUntil; 52 | 53 | mapping(uint256 => Pack) public packs; 54 | /// The number of created packs. 55 | uint256 size; 56 | 57 | /// Thrown when a mint is attempt before the sale opens. 58 | error Closed(uint256 closedUntil); 59 | /// Thrown when the passed pack id is invalid (greater or equal than `size`). 60 | error InvalidPack(); 61 | /// Thrown when a mint would exceed the pack supply. 62 | error InsufficientSupply(uint256 packId); 63 | 64 | constructor(IStickers _stickers, address admin, address _treasury, Pack[] memory _packs) 65 | Treasury(_treasury) 66 | { 67 | stickers = _stickers; 68 | _initializeOwner(admin); 69 | 70 | uint256 length = _packs.length; 71 | for (uint256 i; i < length;) { 72 | _create(_packs[i]); 73 | unchecked { 74 | i++; 75 | } 76 | } 77 | } 78 | 79 | /// @notice List available packs. 80 | function getPacks() external view returns (Pack[] memory) { 81 | Pack[] memory output = new Pack[](size); 82 | for (uint256 i; i < size;) { 83 | output[i] = packs[i]; 84 | unchecked { 85 | i++; 86 | } 87 | } 88 | 89 | return output; 90 | } 91 | 92 | /// @notice Checks if the sale is open. 93 | function isClosed() public view returns (bool) { 94 | return closedUntil == 0 || block.timestamp < closedUntil; 95 | } 96 | 97 | /// @dev Internal unprotected pack creation. 98 | function _create(Pack memory _pack) internal { 99 | packs[size++] = _pack; 100 | } 101 | 102 | /// @notice Create a new pack. 103 | /// @dev Requirements: 104 | /// - Only owner can create new packs. 105 | function create(Pack memory _pack) external onlyOwner { 106 | _create(_pack); 107 | } 108 | 109 | /// @notice Purchase a Stickers pack. 110 | /// @dev Requirements: 111 | /// - Sale must be open. 112 | /// - Pack ID must exist. 113 | /// - Transaction value must be greater or equal than the pack price. 114 | function purchase(uint256 packId, address recipient) external payable forwarder { 115 | if (isClosed()) { 116 | revert Closed(closedUntil); 117 | } 118 | 119 | if (packId >= size) { 120 | revert InvalidPack(); 121 | } 122 | 123 | Pack memory pack = packs[packId]; 124 | 125 | if (pack.minted + 1 > pack.totalSupply) { 126 | revert InsufficientSupply(packId); 127 | } 128 | 129 | if (msg.value < pack.price) { 130 | revert InsufficientValue(msg.value, pack.price); 131 | } 132 | 133 | uint256 quantity = 134 | pack.content.common + pack.content.rare + pack.content.epic + pack.content.legendary; 135 | 136 | StickerRarity[] memory rarities = new StickerRarity[](quantity); 137 | 138 | uint256 requestId; 139 | for (uint256 i; i < pack.content.common;) { 140 | rarities[requestId++] = StickerRarity.COMMON; 141 | unchecked { 142 | i++; 143 | } 144 | } 145 | for (uint256 i; i < pack.content.rare;) { 146 | rarities[requestId++] = StickerRarity.RARE; 147 | unchecked { 148 | i++; 149 | } 150 | } 151 | for (uint256 i; i < pack.content.epic;) { 152 | rarities[requestId++] = StickerRarity.EPIC; 153 | unchecked { 154 | i++; 155 | } 156 | } 157 | for (uint256 i; i < pack.content.legendary;) { 158 | rarities[requestId++] = StickerRarity.LEGENDARY; 159 | unchecked { 160 | i++; 161 | } 162 | } 163 | 164 | stickers.mint(recipient, rarities); 165 | packs[packId].minted++; 166 | } 167 | 168 | /// @notice Restock the sale items. 169 | /// @param refills Array of the modifications to apply. 170 | /// @param _closedUntil Update the opening of the sale; a date in the past opens the sale immediately. 171 | /// @dev Requirements: 172 | /// - msg.sender must have the `SELLER` role or be the owner 173 | function restock(Refill[] memory refills, uint256 _closedUntil) external onlyRolesOrOwner(SELLER) { 174 | uint256 length = refills.length; 175 | for (uint256 i; i < length;) { 176 | Pack memory current = packs[refills[i].packId]; 177 | current.price = refills[i].price; 178 | current.totalSupply = current.minted + refills[i].quantity; 179 | current.supply = refills[i].quantity; 180 | packs[refills[i].packId] = current; 181 | unchecked { 182 | i++; 183 | } 184 | } 185 | 186 | closedUntil = _closedUntil; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/tokens/IPOK.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import { IAccessControl } from "openzeppelin/access/IAccessControl.sol"; 5 | import { IERC20 } from "openzeppelin/token/ERC20/IERC20.sol"; 6 | 7 | /// @title IPOK 8 | /// @author Mathieu Bour, Dusan Zdravkovic for Pooky Labs Ltd. 9 | /// 10 | /// @notice Minimal $POK ERC20 token interface. 11 | interface IPOK is IAccessControl, IERC20 { 12 | /// @notice Mint an arbitrary amount of $POK to an account. 13 | /// @dev Requirements: 14 | /// - only MINTER role can mint $POK tokens 15 | function mint(address to, uint256 amount) external; 16 | 17 | /// @notice Burn an arbitrary amount of $POK of an sender account. 18 | /// It is acknowledged that burning directly from the user wallet is anti-pattern 19 | /// but since $POK is soulbounded, this allow to skip the ERC20 approve call. 20 | /// @dev Requirements: 21 | /// - only BURNER role can burn $POK tokens 22 | function burn(address to, uint256 amount) external; 23 | } 24 | -------------------------------------------------------------------------------- /src/tokens/POK.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | import { ERC20 } from "openzeppelin/token/ERC20/ERC20.sol"; 5 | import { AccessControl } from "openzeppelin/access/AccessControl.sol"; 6 | import { IPOK } from "@/tokens/IPOK.sol"; 7 | 8 | /// @title POK 9 | /// @author Mathieu Bour, Dusan Zdravkovic for Pooky Labs Ltd. 10 | /// 11 | /// @notice POK is ERC20 token used inside of the game, $POK is soul-bounded and serves as in-game currency. 12 | /// Mintable by other Pooky game contracts. 13 | /// 14 | /// @dev Implemented roles: 15 | /// - DEFAULT_ADMIN_ROLE can add/remove roles, it will be granted to the deployer and then quickly transferred to a 16 | /// Gnosis Safe multi-signature wallet. 17 | /// - MINTER role can freely mint new $POK tokens. 18 | /// - BURNER role can freely burning existing $POK tokens. 19 | contract POK is IPOK, ERC20, AccessControl { 20 | /// Role that allows to freely mint new $POK tokens 21 | bytes32 public constant MINTER = keccak256("MINTER"); 22 | /// Role that allows to freely burning existing $POK tokens 23 | bytes32 public constant BURNER = keccak256("BURNER"); 24 | 25 | /// Thrown when a POK transfer is attempted. 26 | error Soulbounded(); 27 | 28 | constructor(address _admin) ERC20("POK", "POK") { 29 | _grantRole(DEFAULT_ADMIN_ROLE, _admin); 30 | } 31 | 32 | /// @notice Mint an arbitrary amount of $POK to an account. 33 | /// @dev Requirements: 34 | /// - only MINTER role can mint $POK tokens 35 | function mint(address to, uint256 amount) external onlyRole(MINTER) { 36 | _mint(to, amount); 37 | } 38 | 39 | /// @notice Burn an arbitrary amount of $POK of an sender account. 40 | /// It is acknowledged that burning directly from the user wallet is anti-pattern 41 | /// but since $POK is soulbounded, this allow to skip the ERC20 approve call. 42 | /// @dev Requirements: 43 | /// - only BURNER role can burn $POK tokens 44 | function burn(address to, uint256 amount) external onlyRole(BURNER) { 45 | _burn(to, amount); 46 | } 47 | 48 | /// @dev Restrict the $POK transfers between accounts. 49 | /// Requirements: 50 | /// - Transfer between accounts if they are disabled. 51 | /// - Mints and burns are always allowed. 52 | function _beforeTokenTransfer(address from, address to, uint256) internal pure override { 53 | if (from == address(0) || to == address(0)) return; 54 | revert Soulbounded(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/types/VRFConfig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { VRFCoordinatorV2Interface } from "chainlink/interfaces/VRFCoordinatorV2Interface.sol"; 5 | 6 | struct VRFConfig { 7 | VRFCoordinatorV2Interface coordinator; 8 | bytes32 keyHash; 9 | uint64 subcriptionId; 10 | uint16 minimumRequestConfirmations; 11 | uint32 callbackGasLimit; 12 | } 13 | -------------------------------------------------------------------------------- /test/BaseTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { Test } from "forge-std/Test.sol"; 5 | 6 | abstract contract BaseTest is Test { 7 | string internal root; 8 | 9 | constructor() { 10 | root = vm.projectRoot(); 11 | } 12 | 13 | function loadDataset(string memory file) internal view returns (bytes memory) { 14 | string memory path = string.concat(root, "/test/datasets/", file); 15 | return vm.parseJson(vm.readFile(path)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/common/Energy.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { Ownable } from "solady/auth/Ownable.sol"; 5 | import { Base64 } from "solady/utils/Base64.sol"; 6 | import { LibString } from "solady/utils/LibString.sol"; 7 | import { Energy } from "@/common/Energy.sol"; 8 | import { ITreasury } from "@/common/ITreasury.sol"; 9 | import { BaseTest } from "@test/BaseTest.sol"; 10 | 11 | struct Attribute { 12 | string trait_type; 13 | uint256 value; 14 | } 15 | 16 | struct Metadata { 17 | Attribute[] attributes; 18 | string description; 19 | string name; 20 | } 21 | 22 | contract EnergyTest is BaseTest { 23 | using LibString for uint256; 24 | 25 | address admin = makeAddr("admin"); 26 | address operator = makeAddr("operator"); 27 | address treasury = makeAddr("treasury"); 28 | address player = makeAddr("player"); 29 | 30 | uint256 defaultPricing = 5.15 ether; 31 | 32 | Energy energy; 33 | 34 | function setUp() public { 35 | vm.startPrank(admin); 36 | address[] memory operators = new address[](1); 37 | operators[0] = operator; 38 | energy = new Energy(admin, operators, treasury, defaultPricing); 39 | vm.stopPrank(); 40 | } 41 | 42 | function test_setPricing_revertUnauthorized() public { 43 | vm.prank(player); 44 | vm.expectRevert(Ownable.Unauthorized.selector); 45 | energy.setPricing(1); 46 | } 47 | 48 | function test_setPricing_operator_pass() public { 49 | uint256 newPricing = 7 ether; 50 | 51 | assertEq(energy.pricing(), defaultPricing); 52 | vm.prank(operator); 53 | energy.setPricing(newPricing); 54 | assertEq(energy.pricing(), newPricing); 55 | } 56 | 57 | function test_setPricing_owner_pass() public { 58 | uint256 newPricing = 7 ether; 59 | 60 | assertEq(energy.pricing(), defaultPricing); 61 | vm.prank(admin); 62 | energy.setPricing(newPricing); 63 | assertEq(energy.pricing(), newPricing); 64 | } 65 | 66 | function trim(string calldata str, uint256 start) external pure returns (string memory) { 67 | return str[start:]; 68 | } 69 | 70 | function test_tokenURI_pass() public { 71 | uint256 quantity = 20; 72 | uint256 tokenId = energy.mint{ value: quantity * energy.pricing() }(quantity, player); 73 | string memory tokenIdStr = tokenId.toString(); 74 | 75 | string memory data64 = 76 | LibString.replace(energy.tokenURI(tokenId), "data:application/json;base64,", ""); 77 | string memory decoded = string(Base64.decode(data64)); 78 | 79 | (Metadata memory metadata) = abi.decode(vm.parseJson(decoded), (Metadata)); 80 | 81 | assertEq(metadata.name, string(abi.encodePacked("Voucher #", tokenIdStr))); 82 | assertEq( 83 | metadata.description, 84 | string(abi.encodePacked("Voucher granting ", quantity.toString(), " Energy on Pooky.gg")) 85 | ); 86 | assertEq(metadata.attributes.length, 1); 87 | assertEq(metadata.attributes[0].trait_type, "Value"); 88 | assertEq(metadata.attributes[0].value, quantity); 89 | } 90 | 91 | function test_mint_revertInsufficientValue() public { 92 | uint256 quantity = 30; 93 | uint256 expectedValue = quantity * energy.pricing(); 94 | uint256 actualValue = expectedValue - 1; 95 | 96 | vm.expectRevert( 97 | abi.encodeWithSelector(ITreasury.InsufficientValue.selector, expectedValue, actualValue) 98 | ); 99 | hoax(player, expectedValue); 100 | energy.mint{ value: actualValue }(quantity, player); 101 | } 102 | 103 | function test_mint_pass() public { 104 | uint256 quantity = 25; 105 | uint256 value = quantity * energy.pricing(); 106 | 107 | hoax(player, value); 108 | uint256 tokenId = energy.mint{ value: value }(quantity, player); 109 | 110 | assertEq(energy.values(tokenId), quantity); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/common/NonceRegistry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { BaseTest } from "@test/BaseTest.sol"; 5 | import { NonceRegistry } from "@/common/NonceRegistry.sol"; 6 | import { AccessControlAssertions } from "@test/utils/AccessControlAssertions.sol"; 7 | 8 | contract NonceRegistryTest is BaseTest, AccessControlAssertions { 9 | address public admin = makeAddr("admin"); 10 | address public operator = makeAddr("operator"); 11 | address public user = makeAddr("user"); 12 | 13 | NonceRegistry public registry; 14 | 15 | function setUp() public { 16 | address[] memory admins = new address[](1); 17 | admins[0] = admin; 18 | address[] memory operators = new address[](1); 19 | operators[0] = operator; 20 | registry = new NonceRegistry(admins, operators); 21 | } 22 | 23 | function test_contructor() public { 24 | assertTrue(registry.hasRole(registry.DEFAULT_ADMIN_ROLE(), admin)); 25 | assertTrue(registry.hasRole(registry.OPERATOR(), operator)); 26 | } 27 | 28 | function testFuzz_set_revertIfNonOperator(bytes32 nonce) public { 29 | vm.expectRevert(); 30 | vm.prank(user); 31 | registry.set(nonce, true); 32 | } 33 | 34 | function testFuzz_set_allowOperators(bytes32 nonce) public { 35 | assertFalse(registry.has(nonce)); 36 | vm.prank(operator); 37 | registry.set(nonce, true); 38 | assertTrue(registry.has(nonce)); 39 | } 40 | 41 | function testFuzz_setBatch_revertIfNonOperator(bytes32 nonce1, bytes32 nonce2, bytes32 nonce3) 42 | public 43 | { 44 | assertFalse(registry.has(nonce1)); 45 | assertFalse(registry.has(nonce2)); 46 | assertFalse(registry.has(nonce3)); 47 | 48 | bytes32[] memory nonces = new bytes32[](3); 49 | nonces[0] = nonce1; 50 | nonces[1] = nonce2; 51 | nonces[2] = nonce3; 52 | 53 | bool[] memory values = new bool[](3); 54 | values[0] = true; 55 | values[1] = true; 56 | values[2] = true; 57 | 58 | expectRevertMissingRole(user, registry.OPERATOR()); 59 | vm.prank(user); 60 | registry.setBatch(nonces, values); 61 | 62 | assertFalse(registry.has(nonce1)); 63 | assertFalse(registry.has(nonce2)); 64 | assertFalse(registry.has(nonce3)); 65 | } 66 | 67 | function testFuzz_setBatch_revertArgumentSizeMismatch( 68 | bytes32 nonce1, 69 | bytes32 nonce2, 70 | bytes32 nonce3 71 | ) public { 72 | assertFalse(registry.has(nonce1)); 73 | assertFalse(registry.has(nonce2)); 74 | assertFalse(registry.has(nonce3)); 75 | 76 | bytes32[] memory nonces = new bytes32[](3); 77 | nonces[0] = nonce1; 78 | nonces[1] = nonce2; 79 | nonces[2] = nonce3; 80 | 81 | bool[] memory values = new bool[](4); 82 | values[0] = true; 83 | values[1] = true; 84 | values[2] = true; 85 | values[3] = true; 86 | 87 | vm.prank(operator); 88 | vm.expectRevert(abi.encodeWithSelector(NonceRegistry.ArgumentSizeMismatch.selector, 3, 4)); 89 | registry.setBatch(nonces, values); 90 | 91 | assertFalse(registry.has(nonce1)); 92 | assertFalse(registry.has(nonce2)); 93 | assertFalse(registry.has(nonce3)); 94 | } 95 | 96 | function testFuzz_setBatch_allowOperators(bytes32 nonce1, bytes32 nonce2, bytes32 nonce3) public { 97 | assertFalse(registry.has(nonce1)); 98 | assertFalse(registry.has(nonce2)); 99 | assertFalse(registry.has(nonce3)); 100 | 101 | bytes32[] memory nonces = new bytes32[](3); 102 | nonces[0] = nonce1; 103 | nonces[1] = nonce2; 104 | nonces[2] = nonce3; 105 | 106 | bool[] memory values = new bool[](3); 107 | values[0] = true; 108 | values[1] = true; 109 | values[2] = true; 110 | 111 | vm.prank(operator); 112 | registry.setBatch(nonces, values); 113 | 114 | assertTrue(registry.has(nonce1)); 115 | assertTrue(registry.has(nonce2)); 116 | assertTrue(registry.has(nonce3)); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /test/datasets/PookyballLevelUp_pricing.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "level": 2, 4 | "increase": 1, 5 | "currentPXP": 10000000000000000000001, 6 | "value": 100000000000000001, 7 | "remainingPXP": 9930660000000000000001, 8 | "expectedPOK": 2689857143000000001 9 | }, 10 | { 11 | "level": 15, 12 | "increase": 9, 13 | "currentPXP": 100000000000000000001, 14 | "value": 6079352620000000001, 15 | "remainingPXP": 0, 16 | "expectedPOK": 258899670600000000001 17 | }, 18 | { 19 | "level": 51, 20 | "increase": 4, 21 | "currentPXP": 1000000000000000000001, 22 | "value": 27000000000000000001, 23 | "remainingPXP": 0, 24 | "expectedPOK": 1303090000000000000001 25 | }, 26 | { 27 | "level": 1, 28 | "increase": 98, 29 | "currentPXP": 100000000000000000000001, 30 | "value": 1337000000000000000001, 31 | "remainingPXP": 0, 32 | "expectedPOK": 160123390000000000000001 33 | }, 34 | { 35 | "level": 65, 36 | "increase": 4, 37 | "currentPXP": 1000000000000000000000001, 38 | "value": 80000000000000000001, 39 | "remainingPXP": 970468010000000000000001, 40 | "expectedPOK": 76845060120000000001 41 | }, 42 | { 43 | "level": 15, 44 | "increase": 5, 45 | "currentPXP": 1000000000000000000001, 46 | "value": 0, 47 | "remainingPXP": 0, 48 | "expectedPOK": 86391694580000000001 49 | }, 50 | { 51 | "level": 15, 52 | "increase": 6, 53 | "currentPXP": 5000000000000000000001, 54 | "value": 0, 55 | "remainingPXP": 3713950000000000000001, 56 | "expectedPOK": 102884005100000000001 57 | }, 58 | { 59 | "level": 15, 60 | "increase": 7, 61 | "currentPXP": 10000000000000000000001, 62 | "value": 4000000000000000001, 63 | "remainingPXP": 8439960000000000000001, 64 | "expectedPOK": 10517202510000000001 65 | }, 66 | { 67 | "level": 20, 68 | "increase": 5, 69 | "currentPXP": 5000000000000000000001, 70 | "value": 2000000000000000001, 71 | "remainingPXP": 3519610000000000000001, 72 | "expectedPOK": 61288407660000000001 73 | }, 74 | { 75 | "level": 20, 76 | "increase": 1, 77 | "currentPXP": 2000000000000000001, 78 | "value": 500000000000000001, 79 | "remainingPXP": 0, 80 | "expectedPOK": 37712854250000000001 81 | }, 82 | { 83 | "level": 30, 84 | "increase": 1, 85 | "currentPXP": 50000000000000000001, 86 | "value": 1470000000000000001, 87 | "remainingPXP": 0, 88 | "expectedPOK": 59435948820000000001 89 | }, 90 | { 91 | "level": 32, 92 | "increase": 3, 93 | "currentPXP": 10000000000000000000001, 94 | "value": 5491192736000000001, 95 | "remainingPXP": 8038860000000000000001, 96 | "expectedPOK": 0 97 | }, 98 | { 99 | "level": 50, 100 | "increase": 2, 101 | "currentPXP": 1500000000000000000001, 102 | "value": 3000000000000000001, 103 | "remainingPXP": 0, 104 | "expectedPOK": 675961007400000000001 105 | }, 106 | { 107 | "level": 0, 108 | "increase": 1, 109 | "currentPXP": 100000000000000000001, 110 | "value": 0, 111 | "remainingPXP": 40000000000000000001, 112 | "expectedPOK": 4800000000000000001 113 | }, 114 | { 115 | "level": 0, 116 | "increase": 1, 117 | "currentPXP": 100000000000000000001, 118 | "value": 80000000000000001, 119 | "remainingPXP": 40000000000000000001, 120 | "expectedPOK": 2514285714000000001 121 | }, 122 | { 123 | "level": 0, 124 | "increase": 1, 125 | "currentPXP": 100000000000000000001, 126 | "value": 168000000000000001, 127 | "remainingPXP": 40000000000000000001, 128 | "expectedPOK": 0 129 | }, 130 | { 131 | "level": 0, 132 | "increase": 1, 133 | "currentPXP": 30000000000000000001, 134 | "value": 0, 135 | "remainingPXP": 0, 136 | "expectedPOK": 8550000000000000001 137 | }, 138 | { 139 | "level": 0, 140 | "increase": 1, 141 | "currentPXP": 30000000000000000001, 142 | "value": 80000000000000001, 143 | "remainingPXP": 0, 144 | "expectedPOK": 6264285714000000001 145 | }, 146 | { 147 | "level": 0, 148 | "increase": 1, 149 | "currentPXP": 30000000000000000001, 150 | "value": 168000000000000001, 151 | "remainingPXP": 0, 152 | "expectedPOK": 3750000000000000001 153 | }, 154 | { 155 | "level": 0, 156 | "increase": 1, 157 | "currentPXP": 0, 158 | "value": 0, 159 | "remainingPXP": 0, 160 | "expectedPOK": 12300000000000000001 161 | }, 162 | { 163 | "level": 0, 164 | "increase": 1, 165 | "currentPXP": 0, 166 | "value": 80000000000000001, 167 | "remainingPXP": 0, 168 | "expectedPOK": 10014285710000000001 169 | }, 170 | { 171 | "level": 0, 172 | "increase": 1, 173 | "currentPXP": 0, 174 | "value": 168000000000000001, 175 | "remainingPXP": 0, 176 | "expectedPOK": 7500000000000000001 177 | } 178 | ] 179 | -------------------------------------------------------------------------------- /test/datasets/PookyballLevelUp_slots.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "level": 1, "expected": 60000 }, 3 | { "level": 2, "expected": 64500 }, 4 | { "level": 3, "expected": 69338 }, 5 | { "level": 4, "expected": 74538 }, 6 | { "level": 5, "expected": 80128 }, 7 | { "level": 6, "expected": 86138 }, 8 | { "level": 7, "expected": 92598 }, 9 | { "level": 8, "expected": 99543 }, 10 | { "level": 9, "expected": 107009 }, 11 | { "level": 10, "expected": 115034 }, 12 | { "level": 11, "expected": 123662 }, 13 | { "level": 12, "expected": 132937 }, 14 | { "level": 13, "expected": 142907 }, 15 | { "level": 14, "expected": 153625 }, 16 | { "level": 15, "expected": 165147 }, 17 | { "level": 16, "expected": 177533 }, 18 | { "level": 17, "expected": 190848 }, 19 | { "level": 18, "expected": 205161 }, 20 | { "level": 19, "expected": 220548 }, 21 | { "level": 20, "expected": 237089 }, 22 | { "level": 21, "expected": 254871 }, 23 | { "level": 22, "expected": 273986 }, 24 | { "level": 23, "expected": 294535 }, 25 | { "level": 24, "expected": 316626 }, 26 | { "level": 25, "expected": 340372 }, 27 | { "level": 26, "expected": 365900 }, 28 | { "level": 27, "expected": 393343 }, 29 | { "level": 28, "expected": 422844 }, 30 | { "level": 29, "expected": 454557 }, 31 | { "level": 30, "expected": 488649 }, 32 | { "level": 31, "expected": 525297 }, 33 | { "level": 32, "expected": 564695 }, 34 | { "level": 33, "expected": 607047 }, 35 | { "level": 34, "expected": 652575 }, 36 | { "level": 35, "expected": 701518 }, 37 | { "level": 36, "expected": 754132 }, 38 | { "level": 37, "expected": 810692 }, 39 | { "level": 38, "expected": 871494 }, 40 | { "level": 39, "expected": 936856 }, 41 | { "level": 40, "expected": 1007120 }, 42 | { "level": 41, "expected": 1082654 }, 43 | { "level": 42, "expected": 1163853 }, 44 | { "level": 43, "expected": 1251142 }, 45 | { "level": 44, "expected": 1344978 }, 46 | { "level": 45, "expected": 1445851 }, 47 | { "level": 46, "expected": 1554290 }, 48 | { "level": 47, "expected": 1670862 }, 49 | { "level": 48, "expected": 1796177 }, 50 | { "level": 49, "expected": 1930890 }, 51 | { "level": 50, "expected": 2075707 }, 52 | { "level": 51, "expected": 2231385 }, 53 | { "level": 52, "expected": 2398739 }, 54 | { "level": 53, "expected": 2578644 }, 55 | { "level": 54, "expected": 2772042 }, 56 | { "level": 55, "expected": 2979945 }, 57 | { "level": 56, "expected": 3203441 }, 58 | { "level": 57, "expected": 3443700 }, 59 | { "level": 58, "expected": 3701977 }, 60 | { "level": 59, "expected": 3979625 }, 61 | { "level": 60, "expected": 4278097 }, 62 | { "level": 61, "expected": 4598954 }, 63 | { "level": 62, "expected": 4943876 }, 64 | { "level": 63, "expected": 5314667 }, 65 | { "level": 64, "expected": 5713267 }, 66 | { "level": 65, "expected": 6141762 }, 67 | { "level": 66, "expected": 6602394 }, 68 | { "level": 67, "expected": 7097573 }, 69 | { "level": 68, "expected": 7629891 }, 70 | { "level": 69, "expected": 8202133 }, 71 | { "level": 70, "expected": 8817293 }, 72 | { "level": 71, "expected": 9478590 }, 73 | { "level": 72, "expected": 10189484 }, 74 | { "level": 73, "expected": 10953696 }, 75 | { "level": 74, "expected": 11775223 }, 76 | { "level": 75, "expected": 12658365 }, 77 | { "level": 76, "expected": 13607742 }, 78 | { "level": 77, "expected": 14628323 }, 79 | { "level": 78, "expected": 15725447 }, 80 | { "level": 79, "expected": 16904855 }, 81 | { "level": 80, "expected": 18172720 }, 82 | { "level": 81, "expected": 19535674 }, 83 | { "level": 82, "expected": 21000849 }, 84 | { "level": 83, "expected": 22575913 }, 85 | { "level": 84, "expected": 24269106 }, 86 | { "level": 85, "expected": 26089289 }, 87 | { "level": 86, "expected": 28045986 }, 88 | { "level": 87, "expected": 30149435 }, 89 | { "level": 88, "expected": 32410642 }, 90 | { "level": 89, "expected": 34841441 }, 91 | { "level": 90, "expected": 37454549 }, 92 | { "level": 91, "expected": 40263640 }, 93 | { "level": 92, "expected": 43283413 }, 94 | { "level": 93, "expected": 46529669 }, 95 | { "level": 94, "expected": 50019394 }, 96 | { "level": 95, "expected": 53770849 }, 97 | { "level": 96, "expected": 57803662 }, 98 | { "level": 97, "expected": 62138937 }, 99 | { "level": 98, "expected": 66799357 }, 100 | { "level": 99, "expected": 71809309 }, 101 | { "level": 100, "expected": 77195007 }, 102 | { "level": 101, "expected": 82984633 }, 103 | { "level": 102, "expected": 89208480 }, 104 | { "level": 103, "expected": 95899116 }, 105 | { "level": 104, "expected": 103091550 }, 106 | { "level": 105, "expected": 110823416 }, 107 | { "level": 106, "expected": 119135172 }, 108 | { "level": 107, "expected": 128070310 }, 109 | { "level": 108, "expected": 137675583 }, 110 | { "level": 109, "expected": 148001252 }, 111 | { "level": 110, "expected": 159101346 }, 112 | { "level": 111, "expected": 171033947 }, 113 | { "level": 112, "expected": 183861493 }, 114 | { "level": 113, "expected": 197651105 }, 115 | { "level": 114, "expected": 212474938 }, 116 | { "level": 115, "expected": 228410558 }, 117 | { "level": 116, "expected": 245541350 }, 118 | { "level": 117, "expected": 263956951 }, 119 | { "level": 118, "expected": 283753723 }, 120 | { "level": 119, "expected": 305035252 }, 121 | { "level": 120, "expected": 327912896 } 122 | ] 123 | -------------------------------------------------------------------------------- /test/datasets/StickersLevelUp_pricing.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "level": 2, 4 | "increase": 1, 5 | "currentPXP": 10000000000000000000001, 6 | "value": 30000000000000001, 7 | "remainingPXP": 9976890000000000000001, 8 | "expectedPOK": 991857100000000001 9 | }, 10 | { 11 | "level": 15, 12 | "increase": 9, 13 | "currentPXP": 100000000000000000001, 14 | "value": 2026450900000000001, 15 | "remainingPXP": 0, 16 | "expectedPOK": 77966556900000000001 17 | }, 18 | { 19 | "level": 51, 20 | "increase": 4, 21 | "currentPXP": 1000000000000000000001, 22 | "value": 9000000000000000001, 23 | "remainingPXP": 0, 24 | "expectedPOK": 351030789700000000001 25 | }, 26 | { 27 | "level": 1, 28 | "increase": 98, 29 | "currentPXP": 100000000000000000000001, 30 | "value": 589000000000000000001, 31 | "remainingPXP": 0, 32 | "expectedPOK": 40945890000000000000001 33 | }, 34 | { 35 | "level": 65, 36 | "increase": 4, 37 | "currentPXP": 1000000000000000000000001, 38 | "value": 27000000000000000001, 39 | "remainingPXP": 990156000000000000000001, 40 | "expectedPOK": 16091210500000000001 41 | }, 42 | { 43 | "level": 15, 44 | "increase": 5, 45 | "currentPXP": 1000000000000000000001, 46 | "value": 0, 47 | "remainingPXP": 656273667400000000001, 48 | "expectedPOK": 27498106600000000001 49 | }, 50 | { 51 | "level": 15, 52 | "increase": 6, 53 | "currentPXP": 5000000000000000000001, 54 | "value": 0, 55 | "remainingPXP": 4571320000000000000001, 56 | "expectedPOK": 34294668400000000001 57 | }, 58 | { 59 | "level": 15, 60 | "increase": 7, 61 | "currentPXP": 10000000000000000000001, 62 | "value": 1300000000000000001, 63 | "remainingPXP": 9479990000000000000001, 64 | "expectedPOK": 4458115100000000001 65 | }, 66 | { 67 | "level": 20, 68 | "increase": 5, 69 | "currentPXP": 5000000000000000000001, 70 | "value": 700000000000000001, 71 | "remainingPXP": 4506540000000000000001, 72 | "expectedPOK": 19477088300000000001 73 | }, 74 | { 75 | "level": 20, 76 | "increase": 1, 77 | "currentPXP": 2000000000000000001, 78 | "value": 150000000000000001, 79 | "remainingPXP": 0, 80 | "expectedPOK": 12880475200000000001 81 | }, 82 | { 83 | "level": 30, 84 | "increase": 1, 85 | "currentPXP": 50000000000000000001, 86 | "value": 490000000000000001, 87 | "remainingPXP": 0, 88 | "expectedPOK": 15645316300000000001 89 | }, 90 | { 91 | "level": 32, 92 | "increase": 3, 93 | "currentPXP": 10000000000000000000001, 94 | "value": 1830397600000000001, 95 | "remainingPXP": 9346290000000000000001, 96 | "expectedPOK": 0 97 | }, 98 | { 99 | "level": 50, 100 | "increase": 2, 101 | "currentPXP": 1500000000000000000001, 102 | "value": 3000000000000000001, 103 | "remainingPXP": 0, 104 | "expectedPOK": 43177478700000000001 105 | }, 106 | { 107 | "level": 0, 108 | "increase": 1, 109 | "currentPXP": 100000000000000000001, 110 | "value": 0, 111 | "remainingPXP": 80000000000000000001, 112 | "expectedPOK": 1600000000000000001 113 | }, 114 | { 115 | "level": 0, 116 | "increase": 1, 117 | "currentPXP": 100000000000000000001, 118 | "value": 20000000000000001, 119 | "remainingPXP": 80000000000000000001, 120 | "expectedPOK": 1028571400000000001 121 | }, 122 | { 123 | "level": 0, 124 | "increase": 1, 125 | "currentPXP": 100000000000000000001, 126 | "value": 20000000000000001, 127 | "remainingPXP": 80000000000000000001, 128 | "expectedPOK": 1028571400000000001 129 | }, 130 | { 131 | "level": 0, 132 | "increase": 1, 133 | "currentPXP": 30000000000000000001, 134 | "value": 0, 135 | "remainingPXP": 10000000000000000001, 136 | "expectedPOK": 1600000000000000001 137 | }, 138 | { 139 | "level": 0, 140 | "increase": 1, 141 | "currentPXP": 30000000000000000001, 142 | "value": 20000000000000001, 143 | "remainingPXP": 10000000000000000001, 144 | "expectedPOK": 1028571400000000001 145 | }, 146 | { 147 | "level": 0, 148 | "increase": 1, 149 | "currentPXP": 30000000000000000001, 150 | "value": 56000000000000001, 151 | "remainingPXP": 10000000000000000001, 152 | "expectedPOK": 0 153 | }, 154 | { 155 | "level": 0, 156 | "increase": 1, 157 | "currentPXP": 0, 158 | "value": 0, 159 | "remainingPXP": 0, 160 | "expectedPOK": 4100000000000000001 161 | }, 162 | { 163 | "level": 0, 164 | "increase": 1, 165 | "currentPXP": 0, 166 | "value": 20000000000000001, 167 | "remainingPXP": 0, 168 | "expectedPOK": 3528571400000000001 169 | }, 170 | { 171 | "level": 0, 172 | "increase": 1, 173 | "currentPXP": 0, 174 | "value": 56000000000000001, 175 | "remainingPXP": 0, 176 | "expectedPOK": 2500000000000000001 177 | } 178 | ] 179 | -------------------------------------------------------------------------------- /test/datasets/StickersLevelUp_slots.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "level": 1, "expected": 20000 }, 3 | { "level": 2, "expected": 21500 }, 4 | { "level": 3, "expected": 23113 }, 5 | { "level": 4, "expected": 24846 }, 6 | { "level": 5, "expected": 26709 }, 7 | { "level": 6, "expected": 28713 }, 8 | { "level": 7, "expected": 30866 }, 9 | { "level": 8, "expected": 33181 }, 10 | { "level": 9, "expected": 35670 }, 11 | { "level": 10, "expected": 38345 }, 12 | { "level": 11, "expected": 41221 }, 13 | { "level": 12, "expected": 44312 }, 14 | { "level": 13, "expected": 47636 }, 15 | { "level": 14, "expected": 51208 }, 16 | { "level": 15, "expected": 55049 }, 17 | { "level": 16, "expected": 59178 }, 18 | { "level": 17, "expected": 63616 }, 19 | { "level": 18, "expected": 68387 }, 20 | { "level": 19, "expected": 73516 }, 21 | { "level": 20, "expected": 79030 }, 22 | { "level": 21, "expected": 84957 }, 23 | { "level": 22, "expected": 91329 }, 24 | { "level": 23, "expected": 98178 }, 25 | { "level": 24, "expected": 105542 }, 26 | { "level": 25, "expected": 113457 }, 27 | { "level": 26, "expected": 121967 }, 28 | { "level": 27, "expected": 131114 }, 29 | { "level": 28, "expected": 140948 }, 30 | { "level": 29, "expected": 151519 }, 31 | { "level": 30, "expected": 162883 }, 32 | { "level": 31, "expected": 175099 }, 33 | { "level": 32, "expected": 188232 }, 34 | { "level": 33, "expected": 202349 }, 35 | { "level": 34, "expected": 217525 }, 36 | { "level": 35, "expected": 233839 }, 37 | { "level": 36, "expected": 251377 }, 38 | { "level": 37, "expected": 270231 }, 39 | { "level": 38, "expected": 290498 }, 40 | { "level": 39, "expected": 312285 }, 41 | { "level": 40, "expected": 335707 }, 42 | { "level": 41, "expected": 360885 }, 43 | { "level": 42, "expected": 387951 }, 44 | { "level": 43, "expected": 417047 }, 45 | { "level": 44, "expected": 448326 }, 46 | { "level": 45, "expected": 481950 }, 47 | { "level": 46, "expected": 518097 }, 48 | { "level": 47, "expected": 556954 }, 49 | { "level": 48, "expected": 598726 }, 50 | { "level": 49, "expected": 643630 }, 51 | { "level": 50, "expected": 691902 }, 52 | { "level": 51, "expected": 743795 }, 53 | { "level": 52, "expected": 799580 }, 54 | { "level": 53, "expected": 859548 }, 55 | { "level": 54, "expected": 924014 }, 56 | { "level": 55, "expected": 993315 }, 57 | { "level": 56, "expected": 1067814 }, 58 | { "level": 57, "expected": 1147900 }, 59 | { "level": 58, "expected": 1233992 }, 60 | { "level": 59, "expected": 1326542 }, 61 | { "level": 60, "expected": 1426032 }, 62 | { "level": 61, "expected": 1532985 }, 63 | { "level": 62, "expected": 1647959 }, 64 | { "level": 63, "expected": 1771556 }, 65 | { "level": 64, "expected": 1904422 }, 66 | { "level": 65, "expected": 2047254 }, 67 | { "level": 66, "expected": 2200798 }, 68 | { "level": 67, "expected": 2365858 }, 69 | { "level": 68, "expected": 2543297 }, 70 | { "level": 69, "expected": 2734044 }, 71 | { "level": 70, "expected": 2939098 }, 72 | { "level": 71, "expected": 3159530 }, 73 | { "level": 72, "expected": 3396495 }, 74 | { "level": 73, "expected": 3651232 }, 75 | { "level": 74, "expected": 3925074 }, 76 | { "level": 75, "expected": 4219455 }, 77 | { "level": 76, "expected": 4535914 }, 78 | { "level": 77, "expected": 4876108 }, 79 | { "level": 78, "expected": 5241816 }, 80 | { "level": 79, "expected": 5634952 }, 81 | { "level": 80, "expected": 6057573 }, 82 | { "level": 81, "expected": 6511891 }, 83 | { "level": 82, "expected": 7000283 }, 84 | { "level": 83, "expected": 7525304 }, 85 | { "level": 84, "expected": 8089702 }, 86 | { "level": 85, "expected": 8696430 }, 87 | { "level": 86, "expected": 9348662 }, 88 | { "level": 87, "expected": 10049812 }, 89 | { "level": 88, "expected": 10803547 }, 90 | { "level": 89, "expected": 11613814 }, 91 | { "level": 90, "expected": 12484850 }, 92 | { "level": 91, "expected": 13421213 }, 93 | { "level": 92, "expected": 14427804 }, 94 | { "level": 93, "expected": 15509890 }, 95 | { "level": 94, "expected": 16673131 }, 96 | { "level": 95, "expected": 17923616 }, 97 | { "level": 96, "expected": 19267887 }, 98 | { "level": 97, "expected": 20712979 }, 99 | { "level": 98, "expected": 22266452 }, 100 | { "level": 99, "expected": 23936436 }, 101 | { "level": 100, "expected": 25731669 }, 102 | { "level": 101, "expected": 27661544 }, 103 | { "level": 102, "expected": 29736160 }, 104 | { "level": 103, "expected": 31966372 }, 105 | { "level": 104, "expected": 34363850 }, 106 | { "level": 105, "expected": 36941139 }, 107 | { "level": 106, "expected": 39711724 }, 108 | { "level": 107, "expected": 42690103 }, 109 | { "level": 108, "expected": 45891861 }, 110 | { "level": 109, "expected": 49333751 }, 111 | { "level": 110, "expected": 53033782 }, 112 | { "level": 111, "expected": 57011316 }, 113 | { "level": 112, "expected": 61287164 }, 114 | { "level": 113, "expected": 65883702 }, 115 | { "level": 114, "expected": 70824979 }, 116 | { "level": 115, "expected": 76136853 }, 117 | { "level": 116, "expected": 81847117 }, 118 | { "level": 117, "expected": 87985650 }, 119 | { "level": 118, "expected": 94584574 }, 120 | { "level": 119, "expected": 101678417 }, 121 | { "level": 120, "expected": 109304299 } 122 | ] 123 | -------------------------------------------------------------------------------- /test/pookyball/PookyballReroll.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { Ownable } from "solady/auth/Ownable.sol"; 5 | import { ECDSA } from "solady/utils/ECDSA.sol"; 6 | import { BaseTest } from "@test/BaseTest.sol"; 7 | import { PookyballSetup } from "@test/setup/PookyballSetup.sol"; 8 | import { ITreasury } from "@/common/ITreasury.sol"; 9 | import { Signer } from "@/common/Signer.sol"; 10 | import { NonceRegistry } from "@/common/NonceRegistry.sol"; 11 | import { PookyballRarity } from "@/pookyball/IPookyball.sol"; 12 | import { PookyballReroll } from "@/pookyball/PookyballReroll.sol"; 13 | 14 | contract PookyballRerollTest is BaseTest, PookyballSetup { 15 | using ECDSA for bytes32; 16 | 17 | /// Fired when a Pookyball stats have been rerolled. 18 | event Reroll(uint256 indexed tokenId, uint256 price); 19 | 20 | address admin = makeAddr("admin"); 21 | address treasury = makeAddr("treasury"); 22 | address signer; 23 | uint256 privateKey; 24 | address user1 = makeAddr("user1"); 25 | address user2 = makeAddr("user2"); 26 | 27 | PookyballReroll reroll; 28 | NonceRegistry nonces; 29 | 30 | function setUp() public { 31 | (signer, privateKey) = makeAddrAndKey("signer"); 32 | 33 | address[] memory admins = new address[](1); 34 | admins[0] = admin; 35 | address[] memory operators = new address[](0); 36 | nonces = new NonceRegistry(admins, operators); 37 | reroll = new PookyballReroll(pookyball, nonces, admin, signer, treasury); 38 | 39 | vm.startPrank(admin); 40 | nonces.grantRole(nonces.OPERATOR(), address(reroll)); 41 | vm.stopPrank(); 42 | } 43 | 44 | /// Sign the tokenId and the price for a reroll. 45 | function sign(uint256 tokenId, uint256 price, bytes32 nonce, address target) 46 | internal 47 | view 48 | returns (bytes memory) 49 | { 50 | bytes32 hash = keccak256(abi.encode(tokenId, price, nonce, target)).toEthSignedMessageHash(); 51 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, hash); 52 | return abi.encodePacked(r, s, v); 53 | } 54 | 55 | function test_reroll_revertOwnershipRequired() public { 56 | uint256 tokenId = mintPookyball(user1, PookyballRarity.COMMON); 57 | bytes32 nonce = keccak256("test_reroll_revertOwnershipRequired"); 58 | uint256 price = 50 ether; 59 | bytes memory signature = sign(tokenId, price, nonce, address(reroll)); 60 | 61 | uint256 balanceBefore = treasury.balance; 62 | 63 | hoax(user2, price); 64 | vm.expectRevert(abi.encodeWithSelector(PookyballReroll.OwnershipRequired.selector, tokenId)); 65 | reroll.reroll{ value: price }(tokenId, price, nonce, signature); 66 | 67 | assertEq(treasury.balance, balanceBefore); 68 | } 69 | 70 | function test_reroll_revertNonceAlreadyUsed() public { 71 | uint256 tokenId = mintPookyball(user1, PookyballRarity.COMMON); 72 | bytes32 nonce = keccak256("test_reroll_revertNonceAlreadyUsed"); 73 | uint256 price = 50 ether; 74 | bytes memory signature = sign(tokenId, price, nonce, address(reroll)); 75 | 76 | // First call: pass 77 | uint256 balanceBefore = treasury.balance; 78 | 79 | hoax(user1, price); 80 | 81 | vm.expectEmit(address(reroll)); 82 | emit Reroll(tokenId, price); 83 | 84 | reroll.reroll{ value: price }(tokenId, price, nonce, signature); 85 | 86 | assertEq(treasury.balance, balanceBefore + price); 87 | 88 | // Second call: revert 89 | balanceBefore = treasury.balance; 90 | 91 | hoax(user1, price); 92 | 93 | vm.expectRevert(abi.encodeWithSelector(PookyballReroll.NonceAlreadyUsed.selector, nonce)); 94 | reroll.reroll{ value: price }(tokenId, price, nonce, signature); 95 | 96 | assertEq(treasury.balance, balanceBefore); 97 | } 98 | 99 | function test_reroll_revertInsufficientValue() public { 100 | uint256 tokenId = mintPookyball(user1, PookyballRarity.COMMON); 101 | bytes32 nonce = keccak256("test_reroll_revertInsufficientValue"); 102 | uint256 price = 50 ether; 103 | bytes memory signature = sign(tokenId, price, nonce, address(reroll)); 104 | 105 | uint256 value = price - 1; 106 | uint256 balanceBefore = treasury.balance; 107 | 108 | hoax(user1, price); 109 | vm.expectRevert(abi.encodeWithSelector(ITreasury.InsufficientValue.selector, price, value)); 110 | reroll.reroll{ value: value }(tokenId, price, nonce, signature); 111 | 112 | assertEq(treasury.balance, balanceBefore); 113 | } 114 | 115 | function test_reroll_revertInvalidSignature() public { 116 | uint256 tokenId = mintPookyball(user1, PookyballRarity.COMMON); 117 | bytes32 nonce = keccak256("test_reroll_revertInvalidSignature"); 118 | uint256 price = 50 ether; 119 | bytes memory signature = sign(tokenId, price, nonce, address(reroll)); 120 | 121 | uint256 value = price - 1; 122 | uint256 balanceBefore = treasury.balance; 123 | 124 | hoax(user1, price); 125 | vm.expectRevert(Signer.InvalidSignature.selector); 126 | reroll.reroll{ value: value }(tokenId, value, nonce, signature); 127 | 128 | assertEq(treasury.balance, balanceBefore); 129 | } 130 | 131 | function test_reroll_pass() public { 132 | uint256 tokenId = mintPookyball(user1, PookyballRarity.COMMON); 133 | bytes32 nonce = keccak256("test_reroll_pass"); 134 | uint256 price = 50 ether; 135 | bytes memory signature = sign(tokenId, price, nonce, address(reroll)); 136 | 137 | uint256 balanceBefore = treasury.balance; 138 | 139 | hoax(user1, price); 140 | 141 | vm.expectEmit(address(reroll)); 142 | emit Reroll(tokenId, price); 143 | 144 | reroll.reroll{ value: price }(tokenId, price, nonce, signature); 145 | 146 | assertEq(treasury.balance, balanceBefore + price); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /test/setup/LevelUpSetup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { ECDSA } from "openzeppelin/utils/cryptography/ECDSA.sol"; 5 | import { BaseTest } from "@test/BaseTest.sol"; 6 | import { POKSetup } from "@test/setup/POKSetup.sol"; 7 | 8 | struct SlotData { 9 | uint256 expected; 10 | uint256 level; 11 | } 12 | 13 | struct LevelData { 14 | uint256 currentPXP; 15 | uint256 expectedPOK; 16 | uint256 increase; 17 | uint256 level; 18 | uint256 remainingPXP; 19 | uint256 value; 20 | } 21 | 22 | abstract contract LevelUpSetup is BaseTest, POKSetup { 23 | using ECDSA for bytes32; 24 | 25 | address internal signer; 26 | uint256 internal privateKey; 27 | 28 | constructor() { 29 | (signer, privateKey) = makeAddrAndKey("signer"); 30 | } 31 | 32 | /// Sign the tokenId and the currentPXP for a level up. 33 | function sign(uint256 tokenId, uint256 currentLevel, uint256 currentPXP, address target) 34 | internal 35 | view 36 | returns (bytes memory) 37 | { 38 | bytes32 hash = 39 | keccak256(abi.encode(tokenId, currentLevel, currentPXP, target)).toEthSignedMessageHash(); 40 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, hash); 41 | return abi.encodePacked(r, s, v); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/setup/POKSetup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { BaseTest } from "@test/BaseTest.sol"; 5 | import { POK } from "@/tokens/POK.sol"; 6 | 7 | abstract contract POKSetup is BaseTest { 8 | POK public pok; 9 | 10 | constructor() { 11 | address admin = makeAddr("admin"); 12 | vm.startPrank(admin); 13 | pok = new POK(admin); 14 | pok.grantRole(pok.MINTER(), makeAddr("minter")); 15 | vm.stopPrank(); 16 | } 17 | 18 | function mintPOK(address recipient, uint256 amount) public { 19 | vm.prank(makeAddr("minter")); 20 | pok.mint(recipient, amount); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/setup/PookyballSetup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { Pookyball } from "@/pookyball/Pookyball.sol"; 5 | import { PookyballRarity } from "@/pookyball/IPookyball.sol"; 6 | import { BaseTest } from "@test/BaseTest.sol"; 7 | import { VRFCoordinatorV2Setup } from "@test/setup/VRFCoordinatorV2Setup.sol"; 8 | 9 | abstract contract PookyballSetup is BaseTest, VRFCoordinatorV2Setup { 10 | Pookyball public pookyball; 11 | 12 | constructor() { 13 | address admin = makeAddr("admin"); 14 | 15 | vm.startPrank(admin); 16 | uint64 subscriptionId = vrf.createSubscription(); 17 | pookyball = new Pookyball( 18 | "https://metadata.pooky.gg/pookyballs/", 19 | "https://metadata.pooky.gg/contracts/Pookyball", 20 | admin, 21 | makeAddr("treasury"), 22 | address(vrf), 23 | keccak256("foobar"), 24 | subscriptionId, 25 | 10, 26 | 2500000 27 | ); 28 | vrf.addConsumer(subscriptionId, address(pookyball)); 29 | 30 | pookyball.grantRole(pookyball.MINTER(), makeAddr("minter")); 31 | pookyball.grantRole(pookyball.GAME(), makeAddr("game")); 32 | vm.stopPrank(); 33 | } 34 | 35 | function randomPookyballRarity(uint8 seed) public view returns (PookyballRarity) { 36 | return randomPookyballRarity(seed, PookyballRarity.COMMON, PookyballRarity.MYTHIC); 37 | } 38 | 39 | function randomPookyballRarity(uint8 seed, PookyballRarity min, PookyballRarity max) 40 | public 41 | view 42 | returns (PookyballRarity) 43 | { 44 | return PookyballRarity(bound(uint8(seed), uint8(min), uint8(max))); 45 | } 46 | 47 | function mintPookyball(address recipient, PookyballRarity rarity) public returns (uint256) { 48 | address[] memory recipients = new address[](1); 49 | recipients[0] = recipient; 50 | PookyballRarity[] memory rarities = new PookyballRarity[](1); 51 | rarities[0] = rarity; 52 | 53 | vm.prank(makeAddr("minter")); 54 | pookyball.mint(recipients, rarities); 55 | return pookyball.lastTokenId(); 56 | } 57 | 58 | function mintPookyball(address recipient) public returns (uint256) { 59 | return mintPookyball(recipient, PookyballRarity.COMMON); 60 | } 61 | 62 | function setPookyballLevel(uint256 tokenId, uint256 level) internal { 63 | vm.prank(makeAddr("game")); 64 | pookyball.setLevel(tokenId, level); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/setup/RewardsSetup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { Ownable } from "solady/auth/Ownable.sol"; 5 | import { ECDSA } from "solady/utils/ECDSA.sol"; 6 | import { NonceRegistry } from "@/common/NonceRegistry.sol"; 7 | import { Rewards, RewardsData } from "@/common/Rewards.sol"; 8 | import { PookyballRarity } from "@/pookyball/IPookyball.sol"; 9 | import { StickerRarity } from "@/stickers/IStickers.sol"; 10 | import { BaseTest } from "@test/BaseTest.sol"; 11 | import { POKSetup } from "@test/setup/POKSetup.sol"; 12 | import { PookyballSetup } from "@test/setup/PookyballSetup.sol"; 13 | import { StickersSetup } from "@test/setup/StickersSetup.sol"; 14 | import { InvalidReceiver } from "@test/utils/InvalidReceiver.sol"; 15 | 16 | contract RewardsSetup is BaseTest, POKSetup, PookyballSetup, StickersSetup { 17 | using ECDSA for bytes32; 18 | 19 | NonceRegistry public registry; 20 | Rewards public rewards; 21 | 22 | address public admin = makeAddr("admin"); 23 | address public operator = makeAddr("operator"); 24 | address public rewarder; 25 | uint256 public privateKey; 26 | address public user = makeAddr("user"); 27 | 28 | event RewardsClaimed(address indexed account, RewardsData rewards, string data); 29 | 30 | constructor() { 31 | (rewarder, privateKey) = makeAddrAndKey("rewarder"); 32 | 33 | address[] memory admins = new address[](1); 34 | admins[0] = admin; 35 | address[] memory operators = new address[](1); 36 | operators[0] = operator; 37 | registry = new NonceRegistry(admins, operators); 38 | 39 | address[] memory rewarders = new address[](1); 40 | rewarders[0] = rewarder; 41 | rewards = new Rewards(pok, pookyball, stickers, registry, admin, rewarders); 42 | 43 | vm.startPrank(admin); 44 | pok.grantRole(pok.MINTER(), address(rewards)); 45 | pookyball.grantRole(pookyball.MINTER(), address(rewards)); 46 | stickers.grantRoles(address(rewards), stickers.MINTER()); 47 | registry.grantRole(registry.OPERATOR(), address(rewards)); 48 | vm.stopPrank(); 49 | } 50 | 51 | function signRewards(address to, RewardsData memory rewardsData, string memory data) 52 | public 53 | view 54 | returns (bytes memory) 55 | { 56 | bytes32 hash = keccak256(abi.encode(to, rewardsData, data)).toEthSignedMessageHash(); 57 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, hash); 58 | return abi.encodePacked(r, s, v); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/setup/StickersControllerSetup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { StickersController } from "@/stickers/StickersController.sol"; 5 | import { IStickers } from "@/stickers/IStickers.sol"; 6 | import { BaseTest } from "@test/BaseTest.sol"; 7 | import { PookyballSetup } from "@test/setup/PookyballSetup.sol"; 8 | import { StickersSetup } from "@test/setup/StickersSetup.sol"; 9 | 10 | abstract contract StickersControllerSetup is BaseTest, StickersSetup, PookyballSetup { 11 | address public linker = makeAddr("linker"); 12 | address public replacer = makeAddr("replacer"); 13 | address public remover = makeAddr("remover"); 14 | 15 | StickersController public controller; 16 | 17 | constructor() { 18 | address admin = makeAddr("admin"); 19 | controller = new StickersController(pookyball, stickers, admin); 20 | 21 | vm.startPrank(admin); 22 | controller.grantRoles(linker, controller.LINKER()); 23 | controller.grantRoles(replacer, controller.REPLACER()); 24 | controller.grantRoles(remover, controller.REMOVER()); 25 | vm.stopPrank(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/setup/StickersSetup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { StickerMint, StickerRarity } from "@/stickers/IStickers.sol"; 5 | import { Stickers } from "@/stickers/Stickers.sol"; 6 | import { VRFConfig } from "@/types/VRFConfig.sol"; 7 | import { VRFCoordinatorV2Setup } from "@test/setup/VRFCoordinatorV2Setup.sol"; 8 | import { BaseTest } from "@test/BaseTest.sol"; 9 | 10 | abstract contract StickersSetup is BaseTest, VRFCoordinatorV2Setup { 11 | Stickers public stickers; 12 | 13 | constructor() { 14 | address admin = makeAddr("admin"); 15 | vm.startPrank(admin); 16 | uint64 subscriptionId = vrf.createSubscription(); 17 | stickers = new Stickers( 18 | admin, 19 | makeAddr("treasury"), 20 | VRFConfig(vrf, keccak256("foobar"), subscriptionId, 10, 2500000) 21 | ); 22 | vrf.addConsumer(subscriptionId, address(stickers)); 23 | 24 | stickers.grantRoles(makeAddr("minter"), stickers.MINTER()); 25 | stickers.grantRoles(makeAddr("game"), stickers.GAME()); 26 | vm.stopPrank(); 27 | } 28 | 29 | function randomStickerRarity(uint256 seed) public view returns (StickerRarity) { 30 | return StickerRarity( 31 | bound(uint256(seed), uint256(StickerRarity.COMMON), uint256(StickerRarity.MYTHIC)) 32 | ); 33 | } 34 | 35 | function mintSticker(address recipient, StickerRarity rarity) public returns (uint256) { 36 | StickerRarity[] memory rarities = new StickerRarity[](1); 37 | rarities[0] = rarity; 38 | 39 | vm.prank(makeAddr("minter")); 40 | stickers.mint(recipient, rarities); 41 | return stickers.nextTokenId() - 1; 42 | } 43 | 44 | function mintSticker(address recipient) public returns (uint256) { 45 | return mintSticker(recipient, StickerRarity.COMMON); 46 | } 47 | 48 | function setStickerLevel(uint256 tokenId, uint248 level) internal { 49 | vm.prank(makeAddr("game")); 50 | stickers.setLevel(tokenId, level); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/setup/VRFCoordinatorV2Setup.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { VRFCoordinatorV2Interface } from "chainlink/interfaces/VRFCoordinatorV2Interface.sol"; 5 | import { VRFCoordinatorV2Mock } from "chainlink/mocks/VRFCoordinatorV2Mock.sol"; 6 | import { BaseTest } from "@test/BaseTest.sol"; 7 | 8 | abstract contract VRFCoordinatorV2Setup is BaseTest { 9 | VRFCoordinatorV2Interface public vrf; 10 | 11 | constructor() { 12 | vrf = new VRFCoordinatorV2Mock(0, 0); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/stickers/StickersController.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { IERC721A } from "ERC721A/IERC721A.sol"; 5 | import { IStickersController } from "@/stickers/IStickersController.sol"; 6 | import { StickersController } from "@/stickers/StickersController.sol"; 7 | import { IStickers } from "@/stickers/IStickers.sol"; 8 | import { BaseTest } from "@test/BaseTest.sol"; 9 | import { StickersControllerSetup } from "@test/setup/StickersControllerSetup.sol"; 10 | 11 | contract StickersControllerTest is BaseTest, StickersControllerSetup { 12 | address public user = makeAddr("user"); 13 | 14 | event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); 15 | 16 | function testFuzz_attach(uint256 stickerRaritySeed, uint8 pookyballRaritySeed) public { 17 | uint256 stickerId = mintSticker(user, randomStickerRarity(stickerRaritySeed)); 18 | uint256 pookyballId = mintPookyball(user, randomPookyballRarity(pookyballRaritySeed)); 19 | 20 | assertEq(controller.slots(pookyballId), new uint[](0)); 21 | 22 | vm.prank(user); 23 | stickers.setApprovalForAll(address(controller), true); 24 | 25 | vm.prank(linker); 26 | vm.expectEmit(true, true, true, true, address(stickers)); 27 | emit Transfer(user, address(controller), stickerId); 28 | controller.attach(stickerId, pookyballId); 29 | 30 | assertEq(stickers.ownerOf(stickerId), address(controller)); 31 | uint256[] memory slots = new uint[](1); 32 | slots[0] = stickerId; 33 | assertEq(controller.slots(pookyballId), slots); 34 | assertEq(controller.attachedTo(stickerId), pookyballId); 35 | } 36 | 37 | function testFuzz_detach(uint256 stickerRaritySeed, uint8 pookyballRaritySeed) public { 38 | uint256 stickerId = mintSticker(user, randomStickerRarity(stickerRaritySeed)); 39 | uint256 pookyballId = mintPookyball(user, randomPookyballRarity(pookyballRaritySeed)); 40 | 41 | vm.prank(user); 42 | stickers.setApprovalForAll(address(controller), true); 43 | 44 | vm.prank(linker); 45 | controller.attach(stickerId, pookyballId); 46 | assertEq(stickers.ownerOf(stickerId), address(controller)); 47 | 48 | vm.prank(remover); 49 | controller.detach(stickerId, user); 50 | assertEq(stickers.ownerOf(stickerId), user); 51 | } 52 | 53 | function testFuzz_replace_revertInvalidSticker( 54 | uint256 stickerSeed1, 55 | uint256 stickerSeed2, 56 | uint8 pookyballSeed 57 | ) public { 58 | uint256 stickerId1 = mintSticker(user, randomStickerRarity(stickerSeed1)); 59 | uint256 stickerId2 = mintSticker(user, randomStickerRarity(stickerSeed2)); 60 | uint256 pookyballId = mintPookyball(user, randomPookyballRarity(pookyballSeed)); 61 | 62 | vm.expectRevert(abi.encodeWithSelector(IStickersController.InvalidSticker.selector, stickerId1)); 63 | vm.prank(replacer); 64 | controller.replace(stickerId2, stickerId1, pookyballId); 65 | } 66 | 67 | function testFuzz_replace_pass(uint256 stickerRaritySeed, uint8 pookyballRaritySeed) public { 68 | uint256 stickerId1 = mintSticker(user, randomStickerRarity(stickerRaritySeed)); 69 | uint256 stickerId2 = 70 | mintSticker(user, randomStickerRarity(stickerRaritySeed & pookyballRaritySeed)); 71 | uint256 pookyballId = mintPookyball(user, randomPookyballRarity(pookyballRaritySeed)); 72 | 73 | vm.prank(user); 74 | stickers.setApprovalForAll(address(controller), true); 75 | 76 | vm.prank(linker); 77 | controller.attach(stickerId1, pookyballId); 78 | assertEq(stickers.ownerOf(stickerId1), address(controller)); 79 | 80 | vm.prank(replacer); 81 | controller.replace(stickerId2, stickerId1, pookyballId); 82 | 83 | // Assert that stickerId1 was burned 84 | vm.expectRevert(IERC721A.OwnerQueryForNonexistentToken.selector); 85 | stickers.ownerOf(stickerId1); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/stickers/StickersLevelUp.fuzz.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { ECDSA } from "solady/utils/ECDSA.sol"; 5 | import { LevelUp, Pricing } from "@/common/LevelUp.sol"; 6 | import { StickerMetadata, StickerRarity } from "@/stickers/IStickers.sol"; 7 | import { StickersLevelUp } from "@/stickers/StickersLevelUp.sol"; 8 | import { BaseTest } from "@test/BaseTest.sol"; 9 | import { LevelUpSetup } from "@test/setup/LevelUpSetup.sol"; 10 | import { POKSetup } from "@test/setup/POKSetup.sol"; 11 | import { StickersSetup } from "@test/setup/StickersSetup.sol"; 12 | 13 | struct SlotData { 14 | uint256 expected; 15 | uint256 level; 16 | } 17 | 18 | contract StickersLevelUpFuzzTest is BaseTest, StickersSetup, LevelUpSetup { 19 | using ECDSA for bytes32; 20 | 21 | address public admin = makeAddr("admin"); 22 | address public game = makeAddr("game"); 23 | address public user = makeAddr("user"); 24 | address public treasury = makeAddr("treasury"); 25 | 26 | StickersLevelUp public levelUp; 27 | 28 | function setUp() public { 29 | vm.startPrank(admin); 30 | levelUp = new StickersLevelUp(stickers, pok, admin, signer, treasury); 31 | levelUp.grantRoles(signer, levelUp.SIGNER()); 32 | stickers.grantRoles(address(levelUp), stickers.GAME()); 33 | pok.grantRole(pok.BURNER(), address(levelUp)); 34 | vm.stopPrank(); 35 | } 36 | 37 | /// Assert that the levelUp function reverts if the user has not enough POK tokens. 38 | function testFuzz_levelUp_revertInsufficientPOK(uint256 raritySeed, uint256 balancePOK) public { 39 | StickerRarity rarity = randomStickerRarity(raritySeed); 40 | uint256 tokenId = mintSticker(user, rarity); 41 | 42 | uint256 currentPXP = 0; 43 | StickerMetadata memory metadata = stickers.metadata(tokenId); 44 | Pricing memory pricing = levelUp.getPricing(uint256(metadata.level), currentPXP, 1, 0); 45 | uint256 requiredPOK = pricing.feePOK + pricing.gapPOK; 46 | balancePOK = bound(balancePOK, 0, pricing.feePOK + pricing.gapPOK - 1); 47 | 48 | vm.prank(user); 49 | vm.expectRevert(abi.encodeWithSelector(LevelUp.InsufficientPOK.selector, requiredPOK, 0)); 50 | levelUp.levelUp( 51 | tokenId, 1, currentPXP, sign(tokenId, metadata.level, currentPXP, address(levelUp)) 52 | ); 53 | } 54 | 55 | /// Assert that the levelUp function reverts if the Sticker is already at the maximum level. 56 | function testFuzz_levelUp_revertMaximumLevelReached(uint256 raritySeed) public { 57 | StickerRarity rarity = randomStickerRarity(raritySeed); 58 | uint256 tokenId = mintSticker(user, rarity); 59 | (, uint256 maxLevel) = levelUp.getParams(tokenId); 60 | StickerMetadata memory metadata = stickers.metadata(tokenId); 61 | 62 | vm.prank(game); 63 | stickers.setLevel(tokenId, uint248(maxLevel)); 64 | 65 | mintPOK(user, 100e18); 66 | uint256 currentPXP = 0; 67 | 68 | vm.prank(user); 69 | vm.expectRevert(abi.encodeWithSelector(LevelUp.MaximumLevelReached.selector, tokenId, maxLevel)); 70 | levelUp.levelUp( 71 | tokenId, 1, currentPXP, sign(tokenId, metadata.level, currentPXP, address(levelUp)) 72 | ); 73 | } 74 | 75 | /// @dev This function computes some intermediate values for the testFuzz_levelUp_pass function. 76 | /// This allow to avoid the following error: 77 | /// Stack too deep. Try compiling with `--via-ir` (cli) or the equivalent `viaIR: true` (standard JSON) 78 | /// while enabling the optimizer. Otherwise, try removing local variables. 79 | function prepare_testFuzz_levelUp_pass( 80 | uint256 raritySeed, 81 | uint256 levelSeed, 82 | uint256 increaseSeed, 83 | uint256 valueSeed 84 | ) internal returns (uint256 tokenId, uint256 level, uint256 increase, uint256 value) { 85 | StickerRarity rarity = randomStickerRarity(raritySeed); 86 | tokenId = mintSticker(user, rarity); 87 | (, uint256 maxLevel) = levelUp.getParams(tokenId); 88 | level = bound(levelSeed, 0, maxLevel - 1); 89 | 90 | vm.prank(game); 91 | stickers.setLevel(tokenId, uint248(level)); 92 | 93 | increase = bound(increaseSeed, 1, maxLevel - level); 94 | value = bound(valueSeed, 0, 1000 ether); 95 | } 96 | 97 | /// Assert that a Sticker can be upgraded from level zero to level one using POK. 98 | function testFuzz_levelUp_pass( 99 | uint256 raritySeed, 100 | uint256 levelSeed, 101 | uint256 increaseSeed, 102 | uint256 valueSeed, 103 | uint256 currentPXP 104 | ) public { 105 | (uint256 tokenId, uint256 level, uint256 increase, uint256 value) = 106 | prepare_testFuzz_levelUp_pass(raritySeed, levelSeed, increaseSeed, valueSeed); 107 | 108 | Pricing memory pricing = levelUp.getPricing(level, currentPXP, increase, value); 109 | uint256 requiredPOK = pricing.feePOK + pricing.gapPOK; 110 | 111 | mintPOK(user, requiredPOK); 112 | 113 | vm.deal(user, value); 114 | vm.prank(user); 115 | levelUp.levelUp{ value: value }( 116 | tokenId, increase, currentPXP, sign(tokenId, level, currentPXP, address(levelUp)) 117 | ); 118 | 119 | StickerMetadata memory metadata = stickers.metadata(tokenId); 120 | assertEq(metadata.level, level + increase); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /test/stickers/StickersSale.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { Ownable } from "solady/auth/Ownable.sol"; 5 | import { ITreasury } from "@/common/ITreasury.sol"; 6 | import { StickersSale, Pack, PackContent, Refill } from "@/stickers/StickersSale.sol"; 7 | import { BaseTest } from "@test/BaseTest.sol"; 8 | import { StickersSetup } from "@test/setup/StickersSetup.sol"; 9 | import { InvalidReceiver } from "@test/utils/InvalidReceiver.sol"; 10 | 11 | contract StickersSaleTest is BaseTest, StickersSetup { 12 | address admin = makeAddr("admin"); 13 | address seller = makeAddr("seller"); 14 | address user = makeAddr("user"); 15 | address treasury = makeAddr("treasury"); 16 | 17 | uint256 date = 1641070800; 18 | 19 | StickersSale sale; 20 | 21 | Pack[] defaultPacks; 22 | Refill[] defaultRefills; 23 | 24 | function setUp() public { 25 | // Default refills 26 | defaultRefills.push(Refill(0, 8 ether, 45)); 27 | defaultRefills.push(Refill(1, 28 ether, 40)); 28 | defaultRefills.push(Refill(2, 112 ether, 12)); 29 | defaultRefills.push(Refill(3, 416 ether, 3)); 30 | 31 | // Default packs 32 | defaultPacks.push(Pack(8 ether, 45, 0, 45, PackContent(2, 0, 0, 0))); 33 | defaultPacks.push(Pack(28 ether, 40, 0, 40, PackContent(3, 1, 0, 0))); 34 | defaultPacks.push(Pack(112 ether, 12, 0, 12, PackContent(8, 1, 1, 0))); 35 | defaultPacks.push(Pack(416 ether, 3, 0, 3, PackContent(15, 3, 1, 1))); 36 | 37 | for (uint256 i; i < defaultPacks.length; i++) { 38 | defaultRefills.push(Refill(i, defaultPacks[i].price, defaultPacks[i].supply)); 39 | } 40 | 41 | sale = new StickersSale(stickers, admin, treasury, defaultPacks); 42 | 43 | vm.startPrank(sale.owner()); 44 | sale.grantRoles(seller, sale.SELLER()); 45 | vm.startPrank(stickers.owner()); 46 | stickers.grantRoles(address(sale), stickers.MINTER()); 47 | vm.stopPrank(); 48 | 49 | vm.deal(user, 1000000 ether); 50 | 51 | vm.warp(date); 52 | vm.prank(seller); 53 | sale.restock(defaultRefills, 1); 54 | } 55 | 56 | function test_getPacks() public { 57 | Pack[] memory packs = sale.getPacks(); 58 | assertEq(packs.length, 4); 59 | assertEq(packs[0].price, 8 ether); 60 | assertEq(packs[3].price, 416 ether); 61 | } 62 | 63 | function test_isClosed_zero() public { 64 | vm.prank(seller); 65 | sale.restock(defaultRefills, 0); // zero means closed 66 | assertTrue(sale.isClosed()); 67 | } 68 | 69 | function test_isClosed_future() public { 70 | vm.warp(date); 71 | 72 | vm.prank(seller); 73 | sale.restock(defaultRefills, date + 1); // closeUntil greater than date => sale is closed 74 | assertTrue(sale.isClosed()); 75 | } 76 | 77 | function test_create_revertOnlyOwner() public { 78 | assertEq(sale.getPacks().length, defaultPacks.length); 79 | 80 | vm.prank(user); 81 | vm.expectRevert(Ownable.Unauthorized.selector); 82 | sale.create(defaultPacks[0]); 83 | } 84 | 85 | function test_create() public { 86 | assertEq(sale.getPacks().length, defaultPacks.length); 87 | 88 | vm.prank(admin); 89 | sale.create(defaultPacks[0]); 90 | assertEq(sale.getPacks().length, defaultPacks.length + 1); 91 | } 92 | 93 | function test_purchase_revertClose() public { 94 | vm.prank(seller); 95 | sale.restock(defaultRefills, 0); // zero means closed 96 | assertTrue(sale.isClosed()); 97 | 98 | uint256 packId = 0; 99 | Pack memory pack = sale.getPacks()[packId]; 100 | vm.prank(user); 101 | vm.expectRevert(abi.encodeWithSelector(StickersSale.Closed.selector, 0)); 102 | sale.purchase{ value: pack.price }(packId, user); 103 | } 104 | 105 | function test_purchase_revertInvalidPack() public { 106 | uint256 packId = sale.getPacks().length; 107 | vm.prank(user); 108 | vm.expectRevert(StickersSale.InvalidPack.selector); 109 | sale.purchase(packId, user); 110 | } 111 | 112 | function test_purchase_revertInsufficientSupply() public { 113 | vm.warp(date); 114 | 115 | uint256 packId = 0; 116 | Refill[] memory faultyRefills = new Refill[](1); 117 | faultyRefills[0] = Refill(packId, defaultPacks[0].price, 0); 118 | 119 | vm.prank(seller); 120 | sale.restock(faultyRefills, 1); 121 | assertFalse(sale.isClosed()); 122 | 123 | Pack memory pack = sale.getPacks()[packId]; 124 | vm.prank(user); 125 | vm.expectRevert(abi.encodeWithSelector(StickersSale.InsufficientSupply.selector, packId)); 126 | sale.purchase{ value: pack.price }(packId, user); 127 | } 128 | 129 | function test_purchase_revertInsufficientValue() public { 130 | uint256 packId = 0; 131 | Pack memory pack = sale.getPacks()[packId]; 132 | vm.prank(user); 133 | uint256 value = pack.price - 1; 134 | vm.expectRevert(abi.encodeWithSelector(ITreasury.InsufficientValue.selector, value, pack.price)); 135 | sale.purchase{ value: value }(packId, user); 136 | } 137 | 138 | function test_purchase_revertTransferFailed() public { 139 | InvalidReceiver invalid = new InvalidReceiver(); 140 | vm.prank(admin); 141 | sale.changeTreasury(address(invalid)); 142 | 143 | uint256 packId = 0; 144 | Pack memory pack = sale.getPacks()[packId]; 145 | 146 | vm.prank(user); 147 | vm.expectRevert( 148 | abi.encodeWithSelector(ITreasury.TransferFailed.selector, address(invalid), pack.price) 149 | ); 150 | sale.purchase{ value: pack.price }(packId, user); 151 | } 152 | 153 | function test_purchase_pass_legend() public { 154 | uint256 packId = 3; // legend pack with all sticker rarities 155 | Pack memory pack = sale.getPacks()[packId]; 156 | 157 | uint256 stickersBefore = stickers.balanceOf(user); 158 | uint256 quantity = 159 | pack.content.common + pack.content.rare + pack.content.epic + pack.content.legendary; 160 | uint256 expectedBalance = stickersBefore + quantity; 161 | 162 | vm.prank(user); 163 | sale.purchase{ value: pack.price }(packId, user); 164 | assertEq(stickers.balanceOf(user), expectedBalance); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /test/tokens/POK.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { BaseTest } from "@test/BaseTest.sol"; 5 | import { POKSetup } from "@test/setup/POKSetup.sol"; 6 | import { POK } from "@/tokens/POK.sol"; 7 | import { AccessControlAssertions } from "@test/utils/AccessControlAssertions.sol"; 8 | 9 | contract POKTest is BaseTest, AccessControlAssertions, POKSetup { 10 | address public admin = makeAddr("admin"); 11 | address public minter = makeAddr("minter"); 12 | address public user1 = makeAddr("user1"); 13 | address public user2 = makeAddr("user2"); 14 | 15 | function testFuzz_mint_revertOnlyRole(uint256 amount) public { 16 | expectRevertMissingRole(user1, pok.MINTER()); 17 | vm.prank(user1); 18 | pok.mint(user1, amount); 19 | } 20 | 21 | function testFuzz_burn_revertOnlyRole(uint256 amount) public { 22 | expectRevertMissingRole(user1, pok.BURNER()); 23 | vm.prank(user1); 24 | pok.burn(user1, amount); 25 | } 26 | 27 | function testFuzz__beforeTokenTransfer_revertSouldbound(uint256 amount) public { 28 | mintPOK(user1, amount); 29 | 30 | vm.expectRevert(POK.Soulbounded.selector); 31 | vm.prank(user1); 32 | pok.transfer(user2, amount); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/utils/AccessControlAssertions.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | import { BaseTest } from "@test/BaseTest.sol"; 5 | import { Strings } from "openzeppelin/utils/Strings.sol"; 6 | 7 | /// @title AccessControlAssertions 8 | /// Utility contract to test OpenZeppelin AccessControl contracts. 9 | contract AccessControlAssertions is BaseTest { 10 | function expectRevertMissingRole(address account, bytes32 role) public { 11 | vm.expectRevert( 12 | abi.encodePacked( 13 | "AccessControl: account ", 14 | Strings.toHexString(account), 15 | " is missing role ", 16 | Strings.toHexString(uint256(role), 32) 17 | ) 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/utils/InvalidReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.21; 3 | 4 | /// @title InvalidReceiver 5 | /// @dev This contract always revert when receiving tokens. This allow to test scenario where a native transfer fails. 6 | contract InvalidReceiver { 7 | receive() external payable { 8 | revert(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "moduleResolution": "NodeNext", 5 | "target": "ES2022", 6 | "sourceMap": true, 7 | "outDir": "dist/", 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["node_modules"] 13 | } --------------------------------------------------------------------------------