├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .solhint.json ├── .solhintignore ├── .yarnrc.yml ├── README.md ├── arguments.js ├── contracts ├── BatchSend.sol ├── Infinity.sol ├── libraries │ ├── InfiniteArt.sol │ ├── InfiniteBags.sol │ ├── InfiniteGenerator.sol │ ├── InfiniteMetadata.sol │ └── Utilities.sol ├── standards │ ├── ERC1155.sol │ └── ERC1155_base.sol └── test │ └── ReceiveBlock.sol ├── data ├── GENESIS_0_RECIPIENTS.json ├── infinities-gas-spent.json ├── infinities-holdings.json ├── infinity-claims.json └── refunds.json ├── deploy ├── 001-infinity.ts ├── 002-batchsend.ts └── 003-mocks.ts ├── hardhat.config.ts ├── helpers ├── arrays.ts ├── constants.ts ├── decode-uri.ts ├── impersonate.ts └── render-pngs.ts ├── package.json ├── tasks ├── airdrop.ts ├── find-seed.ts └── refunds.ts ├── test └── Infinity.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.sol] 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ETHERSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1 2 | COINMARKETCAP_API_KEY=ABC123 3 | 4 | MAINNET_URL=https://eth-mainnet.g.alchemy.com/v2/ABC 5 | GOERLI_URL=https://eth-goerli.g.alchemy.com/v2/ABC 6 | 7 | PRIVATE_KEY=0xabc123abc123abc123abc123abc123abc123abc123abc123abc123abc123abc1 8 | 9 | REPORT_GAS=true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: false, 4 | es2021: true, 5 | mocha: true, 6 | node: true, 7 | }, 8 | plugins: ["@typescript-eslint"], 9 | extends: [ 10 | "standard", 11 | "plugin:prettier/recommended", 12 | "plugin:node/recommended", 13 | ], 14 | parser: "@typescript-eslint/parser", 15 | parserOptions: { 16 | ecmaVersion: 12, 17 | }, 18 | rules: { 19 | "node/no-unsupported-features/es-syntax": [ 20 | "error", 21 | { ignores: ["modules"] }, 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .env 4 | coverage 5 | coverage.json 6 | .yarn 7 | 8 | test/dist/*.svg 9 | test/dist/*.png 10 | test/dist/pngs/*.png 11 | test/dist/*.json 12 | 13 | deployments/hardhat 14 | deployments/localhost 15 | 16 | #Hardhat files 17 | cache 18 | artifacts 19 | typechain-types 20 | 21 | # OpenZeppelin 22 | .openzeppelin/dev-*.json 23 | .openzeppelin/.session 24 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage* 5 | gasReporterOutput.json 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "^0.8.0"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.6.2.cjs 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Infinity (NFT) 2 | 3 | ## setup 4 | 5 | 1. run `yarn` to install project packages 6 | 2. run any hardhat task via `npx hardhat` or `hh` (if you have the [shorthand](https://hardhat.org/hardhat-runner/docs/guides/command-line-completion) installed) 7 | 3. e.g. run tests via `npx hardhat test` 8 | 9 | ## intro 10 | 11 | > *"ages like 0.008 eth..."* 12 | 13 | infinities are an infinitely collectable collectible in the ethereum ecosystem. 14 | 15 | you create infinities by depositing 0.008 eth into the contract. burning an infinity token returns that eth to the owner of the nft. 16 | 17 | the visuals are randomly arranged grids with symbols created of the base shape that makes up the checks and opepen projects, generated and rendered fully onchain. 18 | 19 | ## core functionality 20 | 21 | there are basically three different logic flows in the contract: 22 | 23 | 1. **generate**: create a token based on an eth deposit 24 | 2. **degenerate**: burn a token and withdraw the deposited eth 25 | 3. **regenerate**: burn + remint a token to generate new artwork 26 | 27 | each of those functions has a corresponding _xyzMany_ call, which performs the action on multiple tokens at once. 28 | 29 | ## design considerations 30 | 31 | the contract is a simplified version of a typical OpenZeppelin ERC1155 token. 32 | 33 | the most important adjustment is that infinities cannot be `approved` to operators, like typical NFTs. thus, programmability of infinity tokens is restricted to token `onReceive` hooks. 34 | 35 | a few unused functionalities like beforeTransfer and afterTransfer hooks and approval checks were removed from the base contract, to make token interactions as cheap as possible. 36 | 37 | token attributes and visuals are seeded via token ids, which are generated by using a combination of `prevrandao`, the account address and the remaining gas during mint. 38 | 39 | by using randomness during mint, users cannot specify a specific token id to mint, unless that token already exists. existing tokens can be reminted indefinitely by anyone at any time. over time, this will visualize *"taste"* at the collection level. 40 | in order to save gas during token generation, we don't store whether a token has been minted. in order to validate a token exists, minters have to additionally pass the account address of any existing holder of the token as the token `source`. 41 | 42 | unlike all other accounts, the visualize value creator account (currently `visualizevalue.eth`) has the right to force-mint any token it pleases. 43 | 44 | as a nod to the black check (see checks.art), every 4096th token is a monochromatic, black on white design. token #0 is referred to the "genesis" piece, a full flower single infinity check, and will be airdropped to eligible checks elements (see elements.checks.art) auction bidders. 45 | 46 | ### reasoning for preventing traditional approval flows 47 | 48 | infinities are to be collected, not traded on marketplaces. there is zero cost benefit to do so, so we consciously prevent doing this via the typical flows used for it (approvals). the only time people would trade these is if they don't know they are infinitely mintable on the contract. 49 | 50 | programmability still exists, by utilizing the onReceive hooks outlined in the [EIP](https://eips.ethereum.org/EIPS/eip-1155) (contracts can run arbitrary logic when being sent infinity tokens). 51 | 52 | 53 | ## deployment (bytecode state of `7e4e389...f722ba`) (out of date) 54 | 55 | - salt: `0x5d41dbb04825b039a98d9169dbfc569f510c6e25f4995cb6822e4015f13fd716`; final address: `0x000D39614481839520901aF2eC5bB81F87888888` 56 | - salt: `0x3997c69a39dd451b5503e35287918552c9384b529b80b77476919bfe2def4f36`; final address: `0x0000943A32B3902d5e6C9aF906009c0024148888` 57 | -------------------------------------------------------------------------------- /arguments.js: -------------------------------------------------------------------------------- 1 | import GENESIS_RECIPIENTS from './data/GENESIS_0_RECIPIENTS.json' 2 | 3 | export default [ 4 | GENESIS_RECIPIENTS, 5 | ] 6 | -------------------------------------------------------------------------------- /contracts/BatchSend.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | /// @title Batch send Ether to multiple addresses. 5 | /// @author jalil.eth & backseats.eth 6 | contract BatchSend { 7 | 8 | /// @dev Inform DAPPs of a failed transfer recipient. 9 | event FailedTransfer(address indexed recipient, uint256 amount); 10 | 11 | /// @dev Error for bad input. 12 | error ArrayLengthMismatch(); 13 | 14 | /// @notice Send ether to many addresses. 15 | /// @param recipients The addresses that should receive funds. 16 | /// @param amounts How much wei to send to each address. 17 | function send( 18 | address[] calldata recipients, 19 | uint256[] calldata amounts 20 | ) public payable { 21 | uint256 count = recipients.length; 22 | if (count != amounts.length) revert ArrayLengthMismatch(); 23 | 24 | uint256 failedAmount; 25 | for (uint i; i < count;) { 26 | (bool success,) = payable(recipients[i]).call{value: amounts[i]}(""); 27 | 28 | // Keep track of failed transfers 29 | if (!success) { 30 | failedAmount += amounts[i]; 31 | 32 | emit FailedTransfer(recipients[i], amounts[i]); 33 | } 34 | 35 | unchecked { ++i; } 36 | } 37 | 38 | // If anything failed to send, refund the msg.sender 39 | if (failedAmount > 0) payable(msg.sender).transfer(failedAmount); 40 | } 41 | } 42 | 43 | // LGTM 44 | 45 | // <3 ty 46 | 47 | // anytime man 48 | -------------------------------------------------------------------------------- /contracts/Infinity.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import "./libraries/InfiniteArt.sol"; 5 | import "./libraries/InfiniteGenerator.sol"; 6 | import "./libraries/InfiniteMetadata.sol"; 7 | 8 | import "./standards/ERC1155.sol"; 9 | 10 | // Contributors: 11 | // 12 | // - 0xCygaar 13 | // - 0xG 14 | // - 0xQuit 15 | // - akshatmittal 16 | // - akuti.eth 17 | // - backseats.eth 18 | // - digitaloil.eth 19 | // - FrankNFT.eth 20 | // - haltakov.eth 21 | // - jalil.eth 22 | // - MouseDev.eth 23 | // - tomhirst.eth 24 | // 25 | /// @title Infinity token contract. 26 | contract Infinity is ERC1155 { 27 | 28 | /// @notice The name of the collection. 29 | string public constant name = "Infinity"; 30 | 31 | /// @notice The symbol of the collection. 32 | string public constant symbol = unicode"∞"; 33 | 34 | /// @notice The price of an infinity token. 35 | uint public constant PRICE = 0.008 ether; 36 | 37 | /// @dev VV creator account. 38 | address private constant VV = 0xc8f8e2F59Dd95fF67c3d39109ecA2e2A017D4c8a; 39 | 40 | /// @dev Emitted when minting a token with a message. 41 | event Message(address indexed from, address indexed to, uint256 indexed id, string message); 42 | 43 | /// @dev Raised when the user isn't allowed to mint the given token. 44 | error InvalidToken(); 45 | 46 | /// @dev Raised when the user provides inconsistent arguments. 47 | error InvalidInput(); 48 | 49 | /// @dev Raised when the msg.value does not meet the required amount for the number of tokens minted. 50 | error InvalidDeposit(); 51 | 52 | /// @dev Raised when refunding the user failed. 53 | error FailedSend(); 54 | 55 | /// @dev Instanciate the contract... 56 | constructor(address[] memory genesisRecipients) ERC1155() payable { 57 | _checkDeposit(genesisRecipients.length); 58 | 59 | for (uint i = 0; i < genesisRecipients.length;) { 60 | _mint(genesisRecipients[i], 0, 1, ""); 61 | 62 | unchecked { ++i; } 63 | } 64 | } 65 | 66 | /// @notice Deposit ether, receive random infinities 67 | receive() external payable { 68 | _generateViaDeposit(msg.sender, _randomId()); 69 | } 70 | 71 | /// @notice Create a new infinity check and deposit 0.008 ETH for each token. 72 | /// @param recipient The address that should receive the token. 73 | /// @param message Mint the token with an optional message. 74 | function generate( 75 | address recipient, 76 | string calldata message 77 | ) external payable { 78 | uint id = _randomId(); 79 | 80 | _generateViaDeposit(recipient, id); 81 | 82 | _message(recipient, id, message); 83 | } 84 | 85 | /// @notice Copy an existing infinity check owned by someone and deposit 0.008 ETH for each token. 86 | /// @param source The address of an existing owner of the token. 87 | /// @param recipient The address that should receive the token. 88 | /// @param id The token to mint. 89 | /// @param message Mint the token with an optional message. 90 | function generateExisting( 91 | address source, 92 | address recipient, 93 | uint id, 94 | string calldata message 95 | ) external payable { 96 | _validateId(id, source); 97 | 98 | _generateViaDeposit(recipient, id); 99 | 100 | _message(recipient, id, message); 101 | } 102 | 103 | /// @notice Swap an inifinity token for a new one. 104 | /// @param id The token to burn. 105 | /// @param amount The token amount to burn / recreate. 106 | function regenerate(uint id, uint amount) external { 107 | // Execute burn 108 | _burn(msg.sender, id, amount); 109 | 110 | // Mint a new token 111 | _mint(msg.sender, _randomId(), amount, ""); 112 | } 113 | 114 | /// @notice Destroy the token to withdraw its desposited ETH. 115 | /// @param id The token to destroy. 116 | /// @param amount The amount to degenerate (withdraws 0.008 ETH per item). 117 | function degenerate( 118 | uint id, 119 | uint amount 120 | ) external { 121 | // Execute burn 122 | _burn(msg.sender, id, amount); 123 | 124 | // Withdraw funds 125 | _send(msg.sender, amount * PRICE); 126 | } 127 | 128 | /// @notice Create multiple infinity check tokens and deposit 0.008 ETH in each. 129 | /// @param recipients The addresses that should receive the token. 130 | /// @param amounts The number of tokens to send to each recipient. 131 | function generateMany( 132 | address[] calldata recipients, 133 | uint[] calldata amounts 134 | ) external payable { 135 | _validateCounts(recipients.length, amounts.length); 136 | _checkDeposit(_totalAmount(amounts)); 137 | 138 | for (uint i = 0; i < recipients.length;) { 139 | _validateAmount(amounts[i]); 140 | 141 | _mint(recipients[i], _randomId(), amounts[i], ""); 142 | 143 | unchecked { ++i; } 144 | } 145 | } 146 | 147 | /// @notice Copy multiple infinity check tokens and deposit 0.008 ETH in each. 148 | /// @param sources The addresses of existing owners of each token. 149 | /// @param recipients The addresses that should receive the token. 150 | /// @param ids The tokens to mint. 151 | /// @param amounts The number of tokens to send for each token. 152 | function generateManyExisting( 153 | address[] calldata sources, 154 | address[] calldata recipients, 155 | uint[] calldata ids, 156 | uint[] calldata amounts 157 | ) external payable { 158 | _validateCounts(sources.length, recipients.length, ids.length, amounts.length); 159 | _checkDeposit(_totalAmount(amounts)); 160 | 161 | for (uint i = 0; i < sources.length;) { 162 | _validateId(ids[i], sources[i]); 163 | _validateAmount(amounts[i]); 164 | 165 | _mint(recipients[i], ids[i], amounts[i], ""); 166 | 167 | unchecked { ++i; } 168 | } 169 | } 170 | 171 | /// @notice Swap multiple inifinity tokens for new ones. 172 | /// @param ids The existing tokens that should be destroyed in the process. 173 | /// @param amounts The number of tokens to recreate per token. 174 | function regenerateMany( 175 | uint[] calldata ids, 176 | uint[] calldata amounts 177 | ) external { 178 | _validateCounts(ids.length, amounts.length); 179 | 180 | for (uint i = 0; i < ids.length;) { 181 | _validateAmount(amounts[i]); 182 | 183 | _burn(msg.sender, ids[i], amounts[i]); 184 | _mint(msg.sender, _randomId(), amounts[i], ""); 185 | 186 | unchecked { ++i; } 187 | } 188 | } 189 | 190 | /// @notice Degenerate multiple tokens at once. 191 | /// @param ids The tokens to destroy. 192 | /// @param amounts The amounts to degenerate (withdraws 0.008 ETH per item). 193 | function degenerateMany( 194 | uint[] calldata ids, 195 | uint[] calldata amounts 196 | ) external { 197 | // Ensure sound input 198 | _validateCounts(ids.length, amounts.length); 199 | 200 | // Execute burn 201 | _burnBatch(msg.sender, ids, amounts); 202 | 203 | // Withdraw funds 204 | _send(msg.sender, _totalAmount(amounts) * PRICE); 205 | } 206 | 207 | /// @notice Render SVG of the token. 208 | /// @param id The token to render. 209 | function svg(uint id) external pure returns (string memory) { 210 | return InfiniteArt.renderSVG(InfiniteGenerator.tokenData(id)); 211 | } 212 | 213 | /// @notice Render the encoded token metadata-URI. 214 | /// @param id The token to get metadata for. 215 | function uri(uint id) public pure override returns (string memory) { 216 | return InfiniteMetadata.tokenURI(InfiniteGenerator.tokenData(id)); 217 | } 218 | 219 | /// @notice Supply is (in)finite: (2^256 - 1)^2. 220 | function totalSupply() external pure returns (uint) { return type(uint).max; } 221 | function totalSupply(uint) external pure returns (uint) { return type(uint).max; } 222 | 223 | /// @dev Mint a token n times, based on the amount of ETH sent. 224 | function _generateViaDeposit(address recipient, uint id) internal { 225 | uint amount = msg.value / PRICE; 226 | uint surplus = msg.value % PRICE; 227 | 228 | if (amount == 0) revert InvalidDeposit(); 229 | 230 | _mint(recipient, id, amount, ""); 231 | _send(msg.sender, surplus); 232 | } 233 | 234 | /// @dev Make sure the token exists or VV is the minter. 235 | function _validateId(uint id, address source) internal view { 236 | if (balanceOf(source, id) > 0 || msg.sender == VV) return; 237 | 238 | revert InvalidToken(); 239 | } 240 | 241 | /// @dev Validate that both counts are equal. 242 | function _validateCounts(uint a, uint b) internal pure { 243 | if (a != b) revert InvalidInput(); 244 | } 245 | 246 | /// @dev Validate that all four counts are equal. 247 | function _validateCounts(uint a, uint b, uint c, uint d) internal pure { 248 | if (a != b || a != c || a != d) revert InvalidInput(); 249 | } 250 | 251 | /// @dev Validate that at least one token is interacted with. 252 | function _validateAmount(uint amount) internal pure { 253 | if (amount == 0) revert InvalidInput(); 254 | } 255 | 256 | /// @dev Make a random generative token. 257 | function _randomId() internal view returns (uint) { 258 | return uint(keccak256(abi.encodePacked(block.prevrandao, msg.sender, gasleft()))); 259 | } 260 | 261 | /// @dev Check whether the deposited Ether is a correct {PRICE} multipe of the token {amount} 262 | function _checkDeposit(uint amount) internal { 263 | if (msg.value != amount * PRICE) revert InvalidDeposit(); 264 | } 265 | 266 | /// @dev Get the sum of all given amounts 267 | function _totalAmount(uint[] calldata amounts) internal pure returns (uint amount) { 268 | for (uint i = 0; i < amounts.length;) { 269 | amount += amounts[i]; 270 | 271 | unchecked { ++i; } 272 | } 273 | } 274 | 275 | /// @dev Send ETH to an address 276 | function _send(address to, uint value) internal { 277 | (bool success,) = to.call{ value: value }(""); 278 | 279 | if (! success) revert FailedSend(); 280 | } 281 | 282 | /// @dev Emit a mint message, if provided 283 | function _message(address recipient, uint id, string calldata message) internal { 284 | if (bytes(message).length > 0) { 285 | emit Message(msg.sender, recipient, id, message); 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /contracts/libraries/InfiniteArt.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import "./InfiniteBags.sol"; 5 | import "./Utilities.sol"; 6 | 7 | /** 8 | @title InfiniteArt 9 | @author VisualizeValue 10 | @notice Renders the Infinity visuals. 11 | */ 12 | library InfiniteArt { 13 | 14 | /// @dev Generate the SVG code for an Infinity token. 15 | function renderSVG(Token memory data) public pure returns (string memory) { 16 | return string.concat( 17 | '', 18 | renderStyle(data), 19 | renderDefs(), 20 | '', 21 | '', 22 | renderGrid(), 23 | '', 24 | renderNoise(data), 25 | '', 26 | renderSymbols(data), 27 | '', 28 | '' 29 | ); 30 | } 31 | 32 | /// @dev Render CSS variables. 33 | function renderStyle(Token memory data) public pure returns (string memory) { 34 | return string.concat( 35 | '' 41 | ); 42 | } 43 | 44 | /// @dev Render SVG meta defenitions. 45 | function renderDefs() public pure returns (string memory) { 46 | return string.concat( 47 | '', 48 | '' 49 | '', renderGridRow(), '', 50 | '', 51 | '', 52 | '', 53 | '', 54 | '', 55 | '', 56 | '', 57 | '', 58 | '', 59 | '', 60 | '' 61 | ); 62 | } 63 | 64 | /// @dev Generate the SVG code for the entire 8x8 grid. 65 | function renderGrid() public pure returns (string memory) { 66 | string memory grid; 67 | for (uint256 i; i < 8; i++) { 68 | grid = string.concat( 69 | grid, 70 | '' 71 | ); 72 | } 73 | 74 | return grid; 75 | } 76 | 77 | /// @dev Generate the SVG code for rows in the 8x8 grid. 78 | function renderGridRow() public pure returns (string memory) { 79 | string memory row; 80 | for (uint256 i; i < 8; i++) { 81 | row = string.concat( 82 | row, 83 | '' 84 | ); 85 | } 86 | return row; 87 | } 88 | 89 | /// @dev Render the noise layer. 90 | function renderNoise(Token memory data) public pure returns (string memory) { 91 | return string.concat( 92 | '' 95 | ); 96 | } 97 | 98 | /// @dev Generate SVG code for the symbols. 99 | function renderSymbols(Token memory data) public pure returns (string memory) { 100 | uint space = 800 / data.grid; 101 | uint center = space / 4; 102 | uint width = space / 2; 103 | 104 | string memory symbols; 105 | for (uint i = 0; i < data.count; i++) { 106 | Symbol memory symbol = data.symbols[i]; 107 | 108 | uint baseStroke = symbol.isInfinity ? 8 : 4; 109 | uint stroke = (data.grid < 8 ? baseStroke : baseStroke * 3 / 4) * data.grid / 2; 110 | uint scale = width * 1000 / symbol.formWidth; 111 | 112 | symbol.x = str(i % data.grid * space + center); 113 | symbol.y = str(i / data.grid * space + center); 114 | symbol.stroke = str(stroke); 115 | symbol.center = str(center); 116 | symbol.width = str(width); 117 | symbol.scale = scale < 1000 118 | ? string.concat('0.', str(scale)) 119 | : str(scale / 1000); 120 | 121 | symbols = string.concat(symbols, renderSymbol(symbol)); 122 | } 123 | return symbols; 124 | } 125 | 126 | /// @dev Generate SVG code for the symbols. 127 | function renderSymbol(Symbol memory symbol) public pure returns (string memory) { 128 | symbol.color.rendered = renderColor(symbol.color); 129 | 130 | string memory rendered = symbol.form == 1 ? renderLoop(symbol) 131 | : symbol.form == 2 ? renderInfinitySingle(symbol) 132 | : symbol.form == 3 ? render90Loop(symbol) 133 | : symbol.form == 4 ? renderInfinityPair(symbol) 134 | : symbol.form == 5 ? render180Loop(symbol) 135 | : symbol.form == 8 ? renderInfinityCheck(symbol) 136 | : render360Loop(symbol); 137 | 138 | return string.concat( 139 | '', 143 | rendered, 144 | '' 145 | ); 146 | } 147 | 148 | /// @dev Helper to render a color to its SVG compliant HSL string. 149 | function renderColor(Color memory color) public pure returns (string memory) { 150 | if (bytes(color.rendered).length > 0) return color.rendered; 151 | 152 | return string.concat('hsl(', str(color.h), ' ', str(color.s), '% ', str(color.l), '%)'); 153 | } 154 | 155 | /// @dev Render a single loop symbol. 156 | function renderLoop(Symbol memory symbol) public pure returns (string memory) { 157 | return string.concat( 158 | '' 159 | ); 160 | } 161 | 162 | /// @dev Render two loop symbols, one rotated by 90 degrees. 163 | function render90Loop(Symbol memory symbol) public pure returns (string memory) { 164 | return string.concat( 165 | '', 166 | '', 167 | '', 168 | '' 169 | ); 170 | } 171 | 172 | /// @dev Render two loop symbols, one rotated by 180 degrees. 173 | function render180Loop(Symbol memory symbol) public pure returns (string memory) { 174 | return string.concat( 175 | '', 176 | '', 177 | '', 178 | '' 179 | ); 180 | } 181 | 182 | /// @dev Render four loop symbols to form a square. 183 | function render360Loop(Symbol memory symbol) public pure returns (string memory) { 184 | return string.concat( 185 | '', 186 | '', 187 | '', 188 | '', 189 | '', 190 | '' 191 | ); 192 | } 193 | 194 | /// @dev Check: Render a single infinity. 195 | function renderInfinitySingle(Symbol memory symbol) public pure returns (string memory) { 196 | return string.concat( 197 | '', 198 | '' 199 | '', 200 | '' 201 | '' 202 | ); 203 | } 204 | 205 | /// @dev Double check: Render an infinity pair. 206 | function renderInfinityPair(Symbol memory symbol) public pure returns (string memory) { 207 | return string.concat( 208 | '', 209 | '' 210 | '', 211 | '', 212 | '' 213 | '' 214 | ); 215 | } 216 | 217 | /// @dev Quadruple check: Render an infinity check. 218 | function renderInfinityCheck(Symbol memory symbol) public pure returns (string memory) { 219 | return string.concat( 220 | '', 221 | '' 222 | '', 223 | '', 224 | '', 225 | '', 226 | '' 227 | '' 228 | ); 229 | } 230 | 231 | /// @dev Uint to string helper. 232 | function str(uint n) public pure returns (string memory) { 233 | return Utilities.uint2str(n); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /contracts/libraries/InfiniteBags.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | /** 5 | @title InfiniteBags 6 | @author VisualizeValue 7 | @notice Bags to hold infinity token data. Imo pretty funny... 8 | */ 9 | 10 | /// @dev Bag holding computed token data. 11 | struct Token { 12 | uint seed; 13 | string background; 14 | string gridColor; 15 | uint8 alloy; 16 | uint8 grid; 17 | uint8 count; 18 | uint8 band; 19 | uint8 gradient; 20 | bool continuous; 21 | bool mapColors; 22 | bool light; 23 | Symbol[64] symbols; 24 | } 25 | 26 | /// @dev Bag holding computed symbol data. 27 | struct Symbol { 28 | uint form; 29 | uint16 formWidth; 30 | bool isInfinity; 31 | string rotation; 32 | string stroke; 33 | string center; 34 | string scale; 35 | string width; 36 | string x; 37 | string y; 38 | uint colorIdx; 39 | Color color; 40 | } 41 | 42 | /// @dev Bag holding color data. 43 | struct Color { 44 | uint16 h; 45 | uint16 s; 46 | uint16 l; 47 | string rendered; 48 | } 49 | -------------------------------------------------------------------------------- /contracts/libraries/InfiniteGenerator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import "./InfiniteBags.sol"; 5 | import "./Utilities.sol"; 6 | 7 | /** 8 | @title InfiniteGenerator 9 | @author VisualizeValue 10 | @notice Gathers the data to render Infinity visuals. 11 | */ 12 | library InfiniteGenerator { 13 | 14 | /// @dev 16 distinct colors + void. 15 | uint8 public constant ELEMENTS = 17; 16 | 17 | /// @dev Number of shades for each color. 18 | uint8 public constant SHADES = 4; 19 | 20 | /// @dev Collect relevant rendering data for easy access across functions. 21 | function tokenData(uint tokenId) public pure returns (Token memory data) { 22 | data.seed = tokenId; 23 | data.light = tokenId % 4096 == 0 ? true : false; 24 | data.background = data.light == true ? '#FFFFFF' : '#111111'; 25 | data.gridColor = data.light == true ? '#F5F5F5' : '#19181B'; 26 | data.grid = getGrid(data); 27 | data.count = data.grid ** 2; 28 | data.alloy = getAlloy(data); 29 | data.band = getBand(data); 30 | data.continuous = getContinuous(data); 31 | data.gradient = getGradient(data); 32 | data.mapColors = getColorMap(data); 33 | data.symbols = getSymbols(data); 34 | } 35 | 36 | /// @dev Define the grid for a token. 37 | function getGrid(Token memory data) public pure returns (uint8) { 38 | if (data.seed == 0) return 1; // Genesis token override. 39 | 40 | uint n = Utilities.random(data.seed, 'grid', 160); 41 | 42 | return n < 1 ? 1 43 | : n < 8 ? 2 44 | : n < 32 ? 4 45 | : 8; 46 | } 47 | 48 | /// @dev Define the color band size for a token. 49 | function getBand(Token memory data) public pure returns (uint8) { 50 | // Four times the number of used elements, min 1. 51 | return Utilities.max(data.alloy * SHADES, 1); 52 | } 53 | 54 | /// @dev Whether to map symbols to colors. 55 | function getColorMap(Token memory data) public pure returns (bool) { 56 | // 20% for gradients; 8% for skittles. 57 | return data.gradient > 0 58 | ? Utilities.random(data.seed, 'color_map', 100) < 20 59 | : Utilities.random(data.seed, 'color_map', 100) < 8; 60 | } 61 | 62 | /// @dev Whether color banding is continuous or random. 50/50. 63 | function getContinuous(Token memory data) public pure returns (bool) { 64 | return Utilities.random(data.seed, 'continuous', 2) < 1; 65 | } 66 | 67 | /// @dev Get the number of distinct elements used. 0 for Isolates. 68 | function getAlloy(Token memory data) public pure returns (uint8) { 69 | if (data.grid == 1) return 0; 70 | 71 | uint8 n = uint8(Utilities.random(data.seed, 'alloy', 100)); 72 | 73 | return n >= 56 ? 4 + n % (ELEMENTS - 4) // Complete 74 | : n >= 24 ? 2 // Compound 75 | : n >= 4 ? 1 // Composite 76 | : 0; // Isolate 77 | } 78 | 79 | /// @dev Choose a gradient for the token. 80 | function getGradient(Token memory data) public pure returns (uint8) { 81 | if (data.grid == 1 || data.alloy == 0) return 0; // No gradients for 1x1 or isolate tokens 82 | if (Utilities.random(data.seed, 'gradient', 10) < 8) return 0; // 80% have no gradient 83 | 84 | uint8 options = data.grid == 2 ? 2 : 7; 85 | uint8[7] memory GRADIENTS = data.grid == 2 ? [1, 2, 0, 0, 0, 0, 0] 86 | : data.grid == 4 ? [1, 2, 3, 4, 5, 8, 10] 87 | : [1, 2, 4, 7, 8, 9, 16]; 88 | 89 | return GRADIENTS[Utilities.random(data.seed, 'select_gradient', options)]; 90 | } 91 | 92 | /// @dev Get the symbols for all slots on the grid. 93 | function getSymbols(Token memory data) public pure returns (Symbol[64] memory symbols) { 94 | uint8[7] memory forms = [1, 2, 3, 4, 5, 8, 9]; // Seven distinct symbols. 95 | uint8[7] memory rotationCounts = [2, 4, 4, 2, 2, 0, 0]; // How often we rotate. 96 | 97 | (uint[64] memory colorIndexes, Color[64] memory colors) = getColors(data); 98 | uint[64] memory formColorMap; 99 | 100 | for (uint i = 0; i < data.count; i++) { 101 | symbols[i].colorIdx = colorIndexes[i]; 102 | symbols[i].color = colors[i]; 103 | 104 | uint formIdx = getFormIdx(data, i); 105 | uint form = forms[formIdx]; 106 | if (data.mapColors) { 107 | (formColorMap, form) = setGetMap(formColorMap, symbols[i].colorIdx, form); 108 | } 109 | symbols[i].form = form; 110 | 111 | symbols[i].isInfinity = symbols[i].form % 2 == 0; 112 | symbols[i].formWidth = symbols[i].isInfinity ? 400 : 200; 113 | 114 | uint rotationIncrement = symbols[i].isInfinity ? 45 : 90; 115 | uint rotations = rotationCounts[formIdx] > 0 116 | ? Utilities.random( 117 | data.seed, 118 | string.concat('rotation', str(i)), 119 | rotationCounts[formIdx] 120 | ) 121 | : 0; 122 | symbols[i].rotation = str(rotations * rotationIncrement); 123 | } 124 | } 125 | 126 | /// @dev Get shape of a given symbol of a token. 127 | function getFormIdx(Token memory data, uint i) public pure returns (uint) { 128 | if (data.seed == 0) return 5; // Genesis token is an infinity flower. 129 | 130 | uint random = Utilities.random(data.seed, string.concat('form', str(i)), 10); 131 | if (random == 0) return 0; // 10% Single Loops 132 | 133 | uint8[3] memory common = [1, 3, 5]; // Infinities 134 | uint8[3] memory uncommon = [2, 4, 6]; // Loops 135 | 136 | uint idx = Utilities.random(data.seed, string.concat('form-idx', str(i)), 3); 137 | return random < 8 ? common[idx] : uncommon[idx]; 138 | } 139 | 140 | /// @dev Get all colors available to choose from. 141 | function allColors() public pure returns (Color[68] memory colors) { 142 | // One "Void" color with 4 shades. 143 | uint8[4] memory voidLums = [16, 32, 80, 96]; 144 | for (uint i = 0; i < SHADES; i++) { 145 | colors[i].h = 270; 146 | colors[i].s = 8; 147 | colors[i].l = voidLums[i]; 148 | } 149 | 150 | // 16 distinct colors with 4 shades each. 151 | uint8 count = 4*4; 152 | uint16 startHue = 256; 153 | uint8[4] memory lums = [56, 60, 64, 72]; 154 | for (uint8 i = 0; i < 16; i++) { 155 | uint16 hue = (startHue + 360 * i / count) % 360; 156 | 157 | for(uint8 e = 0; e < 4; e++) { 158 | uint8 idx = 4+i*4+e; 159 | colors[idx].h = hue; 160 | colors[idx].s = 88; 161 | colors[idx].l = lums[e]; 162 | } 163 | } 164 | } 165 | 166 | /// @dev Get the color variations for a specific token. Compute gradients / skittles. 167 | function getColors(Token memory data) public pure returns ( 168 | uint[64] memory colorIndexes, 169 | Color[64] memory colors 170 | ) { 171 | Color[68] memory all = allColors(); 172 | uint[68] memory options = getColorOptions(data); 173 | bool reverse = Utilities.random(data.seed, 'reverse', 2) > 0; 174 | 175 | for (uint i = 0; i < data.count; i++) { 176 | colorIndexes[i] = ( 177 | data.gradient > 0 178 | ? getGradientColor(data, i) 179 | : getRandomColor(data, i) 180 | ) % 68; 181 | 182 | uint idx = reverse ? data.count - 1 - i : i; 183 | 184 | colors[idx] = all[options[colorIndexes[i]]]; 185 | 186 | // Paradoxical, i know. Opepen your eyes. All one. Common fate. 187 | if (data.light) colors[idx].rendered = '#080808'; 188 | } 189 | } 190 | 191 | /// @dev Get the colors to choose from for a given token. 192 | function getColorOptions(Token memory data) public pure returns (uint[68] memory options) { 193 | uint count = Utilities.max(1, data.alloy); 194 | for (uint element = 0; element < count; element++) { 195 | uint idx = element * SHADES; 196 | 197 | uint chosen = data.continuous && element > 0 198 | // Increment previous by one for a continuous band. 199 | ? (options[idx - 1] / SHADES + 1) % ELEMENTS 200 | // Random selection for hard shifts in color. 201 | : Utilities.random(data.seed, string.concat('element', str(element)), ELEMENTS); 202 | 203 | uint chosenIdx = chosen * SHADES; 204 | 205 | for (uint shade = 0; shade < SHADES; shade++) { 206 | options[idx + shade] = chosenIdx + shade; 207 | } 208 | } 209 | } 210 | 211 | /// @dev Compute the gradient colors for a gradient token. 212 | function getGradientColor(Token memory data, uint i) public pure returns (uint) { 213 | uint offset; 214 | if (data.gradient == 3 || data.gradient == 7) { 215 | // Fix angled gradient y-shift. 216 | offset = data.grid + 1; 217 | } 218 | 219 | return ((offset + i) * data.gradient * data.band / data.count) % data.band; 220 | } 221 | 222 | /// @dev Compute colors for a skittle tokens. 223 | function getRandomColor(Token memory data, uint i) public pure returns (uint) { 224 | uint8 max = Utilities.max(SHADES, data.band); 225 | string memory key = data.alloy == 0 ? '0' : str(i); 226 | return Utilities.random(data.seed, string.concat('random_color_', key), max); 227 | } 228 | 229 | /// @dev Helper to keep track of a key value store in memory. 230 | function setGetMap( 231 | uint[64] memory map, uint key, uint value 232 | ) public pure returns (uint[64] memory, uint) { 233 | uint k = key % 64; 234 | 235 | if (map[k] == 0) { 236 | map[k] = value; 237 | } 238 | 239 | return (map, map[k]); 240 | } 241 | 242 | /// @dev Uint to string helper. 243 | function str(uint n) public pure returns (string memory) { 244 | return Utilities.uint2str(n); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /contracts/libraries/InfiniteMetadata.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts/utils/Base64.sol"; 5 | 6 | import "./InfiniteBags.sol"; 7 | import "./InfiniteArt.sol"; 8 | import "./Utilities.sol"; 9 | 10 | /** 11 | @title InfiniteMetadata 12 | @author VisualizeValue 13 | @notice Renders ERC1155 compatible metadata for Infinity tokens. 14 | */ 15 | library InfiniteMetadata { 16 | 17 | /// @dev Render the JSON Metadata for a given Infinity token. 18 | /// @param data The render data for our token 19 | function tokenURI( 20 | Token memory data 21 | ) public pure returns (string memory) { 22 | bytes memory metadata = abi.encodePacked( 23 | '{', 24 | '"name": "Infinity",', 25 | unicode'"description": "∞",', 26 | '"image": ', 27 | '"data:image/svg+xml;base64,', 28 | Base64.encode(abi.encodePacked(InfiniteArt.renderSVG(data))), 29 | '",', 30 | '"attributes": [', attributes(data), ']', 31 | '}' 32 | ); 33 | 34 | return string.concat( 35 | "data:application/json;base64,", 36 | Base64.encode(metadata) 37 | ); 38 | } 39 | 40 | /// @dev Render the JSON atributes for a given Infinity token. 41 | /// @param data The check to render. 42 | function attributes(Token memory data) public pure returns (string memory) { 43 | return string.concat( 44 | trait('Light', light(data.light), ','), 45 | trait('Grid', grid(data), ','), 46 | data.light ? '' : trait('Elements', elements(data), ','), 47 | data.light ? '' : trait('Gradient', gradient(data), ','), 48 | data.light ? '' : trait('Band', band(data), ','), 49 | trait('Symbols', symbols(data), '') 50 | ); 51 | } 52 | 53 | /// @dev Get the value for the 'Light' attribute. 54 | function light(bool on) public pure returns (string memory) { 55 | return on ? 'On' : 'Off'; 56 | } 57 | 58 | /// @dev Get the value for the 'Grid' attribute. 59 | function grid(Token memory data) public pure returns (string memory) { 60 | string memory g = Utilities.uint2str(data.grid); 61 | 62 | return string.concat(g, 'x', g); 63 | } 64 | 65 | /// @dev Get the value for the 'Elements' attribute. 66 | function elements(Token memory data) public pure returns (string memory) { 67 | return data.alloy == 0 ? 'Isolate' 68 | : data.alloy == 1 ? 'Composite' 69 | : data.alloy == 2 ? 'Compound' 70 | : 'Complete'; 71 | } 72 | 73 | /// @dev Get the value for the 'Band' attribute. 74 | function band(Token memory data) public pure returns (string memory) { 75 | return (data.continuous || data.alloy < 2) ? 'Continuous' : 'Cut'; 76 | } 77 | 78 | /// @dev Get the value for the 'Gradient' attribute. 79 | function gradient(Token memory data) public pure returns (string memory) { 80 | return [ 81 | // [0, 1, 2, 3, 4, 5, _, 7, 8, 9, 10, _, _, _, _, _, 16] 82 | 'None', 'Linear', 'Double Linear', 'Angled Down', 'Ordered', 'Angled Up', '', 'Angled Down', 'Linear Z', 83 | 'Angled', 'Angled Up', '', '', '', '', '', 'Double Linear Z' 84 | ][data.gradient]; 85 | } 86 | 87 | /// @dev Get the value for the 'Symbols' attribute. 88 | function symbols(Token memory data) public pure returns (string memory) { 89 | return data.mapColors ? 'Mapped' : 'Random'; 90 | } 91 | 92 | /// @dev Generate the JSON snippet for a single attribute. 93 | /// @param traitType The `trait_type` for this trait. 94 | /// @param traitValue The `value` for this trait. 95 | /// @param append Helper to append a comma. 96 | function trait( 97 | string memory traitType, string memory traitValue, string memory append 98 | ) public pure returns (string memory) { 99 | return string(abi.encodePacked( 100 | '{', 101 | '"trait_type": "', traitType, '",' 102 | '"value": "', traitValue, '"' 103 | '}', 104 | append 105 | )); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /contracts/libraries/Utilities.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.17; 3 | 4 | library Utilities { 5 | /// @dev Zero-index based salted pseudorandom number based on two inputs and max bound 6 | function random(uint256 input, string memory salt, uint256 _max) public pure returns (uint256) { 7 | return (uint256(keccak256(abi.encodePacked(input, salt))) % _max); 8 | } 9 | 10 | /// @dev Convert an integer to a string 11 | function uint2str(uint256 _i) public pure returns (string memory _uintAsString) { 12 | if (_i == 0) { 13 | return "0"; 14 | } 15 | uint256 j = _i; 16 | uint256 len; 17 | while (j != 0) { 18 | ++len; 19 | j /= 10; 20 | } 21 | bytes memory bstr = new bytes(len); 22 | uint256 k = len; 23 | while (_i != 0) { 24 | k = k - 1; 25 | uint8 temp = (48 + uint8(_i - (_i / 10) * 10)); 26 | bytes1 b1 = bytes1(temp); 27 | bstr[k] = b1; 28 | _i /= 10; 29 | } 30 | return string(bstr); 31 | } 32 | 33 | /// @dev Get the larger number 34 | function max(uint8 one, uint8 two) public pure returns (uint8) { 35 | return one > two ? one : two; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /contracts/standards/ERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // Derived from OpenZeppelin Contracts (last updated v4.8.0) (token/ERC1155/ERC1155.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 7 | import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; 8 | import "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; 9 | import "@openzeppelin/contracts/utils/Context.sol"; 10 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; 11 | 12 | /** 13 | * @dev Simplified implementation of the basic standard multi-token. 14 | * See https://eips.ethereum.org/EIPS/eip-1155 15 | * Originally based on code by Enjin: https://github.com/enjin/erc-1155 16 | */ 17 | contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { 18 | // Mapping from token ID to account balances 19 | mapping(uint256 => mapping(address => uint256)) private _balances; 20 | 21 | /** 22 | * @dev See {IERC165-supportsInterface}. 23 | */ 24 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { 25 | return 26 | interfaceId == type(IERC1155).interfaceId || 27 | interfaceId == type(IERC1155MetadataURI).interfaceId || 28 | super.supportsInterface(interfaceId); 29 | } 30 | 31 | /** 32 | * @dev See {IERC1155MetadataURI-uri}. 33 | * 34 | * This implementation returns the same URI for *all* token types. It relies 35 | * on the token type ID substitution mechanism 36 | * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. 37 | * 38 | * Clients calling this function must replace the `\{id\}` substring with the 39 | * actual token type ID. 40 | */ 41 | function uri(uint256) public view virtual override returns (string memory) {} 42 | 43 | /** 44 | * @dev See {IERC1155-balanceOf}. 45 | * 46 | * Requirements: 47 | * 48 | * - `account` cannot be the zero address. 49 | */ 50 | function balanceOf(address account, uint256 id) public view virtual override returns (uint256) { 51 | if (account == address(0)) return 0; 52 | return _balances[id][account]; 53 | } 54 | 55 | /** 56 | * @dev See {IERC1155-balanceOfBatch}. 57 | * 58 | * Requirements: 59 | * 60 | * - `accounts` and `ids` must have the same length. 61 | */ 62 | function balanceOfBatch(address[] memory accounts, uint256[] memory ids) 63 | public 64 | view 65 | virtual 66 | override 67 | returns (uint256[] memory) 68 | { 69 | require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch"); 70 | 71 | uint256[] memory batchBalances = new uint256[](accounts.length); 72 | 73 | for (uint256 i = 0; i < accounts.length; ++i) { 74 | batchBalances[i] = balanceOf(accounts[i], ids[i]); 75 | } 76 | 77 | return batchBalances; 78 | } 79 | 80 | /** 81 | * @notice Infinities are never approved. 82 | * @dev See {IERC1155-setApprovalForAll}. 83 | */ 84 | function setApprovalForAll(address, bool) public virtual override { 85 | revert("No approvals on infinities"); 86 | } 87 | 88 | /** 89 | * @notice Infinities are never approved. 90 | * @dev See {IERC1155-isApprovedForAll}. 91 | */ 92 | function isApprovedForAll(address, address) public view virtual override returns (bool) { 93 | return false; 94 | } 95 | 96 | /** 97 | * @dev See {IERC1155-safeTransferFrom}. 98 | */ 99 | function safeTransferFrom( 100 | address from, 101 | address to, 102 | uint256 id, 103 | uint256 amount, 104 | bytes memory data 105 | ) public virtual override { 106 | require( 107 | from == _msgSender(), 108 | "ERC1155: caller is not token owner" 109 | ); 110 | _safeTransferFrom(from, to, id, amount, data); 111 | } 112 | 113 | /** 114 | * @dev See {IERC1155-safeBatchTransferFrom}. 115 | */ 116 | function safeBatchTransferFrom( 117 | address from, 118 | address to, 119 | uint256[] memory ids, 120 | uint256[] memory amounts, 121 | bytes memory data 122 | ) public virtual override { 123 | require( 124 | from == _msgSender(), 125 | "ERC1155: caller is not token owner" 126 | ); 127 | _safeBatchTransferFrom(from, to, ids, amounts, data); 128 | } 129 | 130 | /** 131 | * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. 132 | * 133 | * Emits a {TransferSingle} event. 134 | * 135 | * Requirements: 136 | * 137 | * - `to` cannot be the zero address. 138 | * - `from` must have a balance of tokens of type `id` of at least `amount`. 139 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the 140 | * acceptance magic value. 141 | */ 142 | function _safeTransferFrom( 143 | address from, 144 | address to, 145 | uint256 id, 146 | uint256 amount, 147 | bytes memory data 148 | ) internal virtual { 149 | require(to != address(0), "ERC1155: transfer to the zero address"); 150 | 151 | uint256 fromBalance = _balances[id][from]; 152 | require(fromBalance >= amount, "ERC1155: insufficient balance for transfer"); 153 | unchecked { 154 | _balances[id][from] = fromBalance - amount; 155 | } 156 | _balances[id][to] += amount; 157 | 158 | emit TransferSingle(from, from, to, id, amount); 159 | 160 | _doSafeTransferAcceptanceCheck(from, from, to, id, amount, data); 161 | } 162 | 163 | /** 164 | * @dev Emits a {TransferBatch} event. 165 | * 166 | * Requirements: 167 | * 168 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the 169 | * acceptance magic value. 170 | */ 171 | function _safeBatchTransferFrom( 172 | address from, 173 | address to, 174 | uint256[] memory ids, 175 | uint256[] memory amounts, 176 | bytes memory data 177 | ) internal virtual { 178 | require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); 179 | require(to != address(0), "ERC1155: transfer to the zero address"); 180 | 181 | for (uint256 i = 0; i < ids.length; ++i) { 182 | uint256 id = ids[i]; 183 | uint256 amount = amounts[i]; 184 | 185 | uint256 fromBalance = _balances[id][from]; 186 | require(fromBalance >= amount, "ERC1155: insufficient balance for transfer"); 187 | unchecked { 188 | _balances[id][from] = fromBalance - amount; 189 | } 190 | _balances[id][to] += amount; 191 | } 192 | 193 | emit TransferBatch(from, from, to, ids, amounts); 194 | 195 | _doSafeBatchTransferAcceptanceCheck(from, from, to, ids, amounts, data); 196 | } 197 | 198 | /** 199 | * @dev Creates `amount` tokens of token type `id`, and assigns them to `to`. 200 | * 201 | * Emits a {TransferSingle} event. 202 | * 203 | * Requirements: 204 | * 205 | * - `to` cannot be the zero address. 206 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the 207 | * acceptance magic value. 208 | */ 209 | function _mint( 210 | address to, 211 | uint256 id, 212 | uint256 amount, 213 | bytes memory data 214 | ) internal virtual { 215 | require(to != address(0), "ERC1155: mint to the zero address"); 216 | 217 | address operator = _msgSender(); 218 | 219 | _balances[id][to] += amount; 220 | emit TransferSingle(operator, address(0), to, id, amount); 221 | 222 | _doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data); 223 | } 224 | 225 | /** 226 | * @dev Destroys `amount` tokens of token type `id` from `from` 227 | * 228 | * Emits a {TransferSingle} event. 229 | * 230 | * Requirements: 231 | * 232 | * - `from` cannot be the zero address. 233 | * - `from` must have at least `amount` tokens of token type `id`. 234 | */ 235 | function _burn( 236 | address from, 237 | uint256 id, 238 | uint256 amount 239 | ) internal virtual { 240 | require(from != address(0), "ERC1155: burn from the zero address"); 241 | 242 | uint256 fromBalance = _balances[id][from]; 243 | require(fromBalance >= amount, "ERC1155: burn amount exceeds balance"); 244 | unchecked { 245 | _balances[id][from] = fromBalance - amount; 246 | } 247 | 248 | emit TransferSingle(_msgSender(), from, address(0), id, amount); 249 | } 250 | 251 | /** 252 | * @dev Batched version of {_burn}. 253 | * 254 | * Emits a {TransferBatch} event. 255 | * 256 | * Requirements: 257 | * 258 | * - `ids` and `amounts` must have the same length. 259 | */ 260 | function _burnBatch( 261 | address from, 262 | uint256[] memory ids, 263 | uint256[] memory amounts 264 | ) internal virtual { 265 | require(from != address(0), "ERC1155: burn from the zero address"); 266 | 267 | address operator = _msgSender(); 268 | 269 | for (uint256 i = 0; i < ids.length; i++) { 270 | uint256 id = ids[i]; 271 | uint256 amount = amounts[i]; 272 | 273 | uint256 fromBalance = _balances[id][from]; 274 | require(fromBalance >= amount, "ERC1155: burn amount exceeds balance"); 275 | unchecked { 276 | _balances[id][from] = fromBalance - amount; 277 | } 278 | } 279 | 280 | emit TransferBatch(operator, from, address(0), ids, amounts); 281 | } 282 | 283 | function _doSafeTransferAcceptanceCheck( 284 | address operator, 285 | address from, 286 | address to, 287 | uint256 id, 288 | uint256 amount, 289 | bytes memory data 290 | ) private { 291 | if (_isContract(to)) { 292 | try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) { 293 | if (response != IERC1155Receiver.onERC1155Received.selector) { 294 | revert("ERC1155: ERC1155Receiver rejected tokens"); 295 | } 296 | } catch Error(string memory reason) { 297 | revert(reason); 298 | } catch { 299 | revert("ERC1155: transfer to non-ERC1155Receiver implementer"); 300 | } 301 | } 302 | } 303 | 304 | function _doSafeBatchTransferAcceptanceCheck( 305 | address operator, 306 | address from, 307 | address to, 308 | uint256[] memory ids, 309 | uint256[] memory amounts, 310 | bytes memory data 311 | ) private { 312 | if (_isContract(to)) { 313 | try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns ( 314 | bytes4 response 315 | ) { 316 | if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { 317 | revert("ERC1155: ERC1155Receiver rejected tokens"); 318 | } 319 | } catch Error(string memory reason) { 320 | revert(reason); 321 | } catch { 322 | revert("ERC1155: transfer to non-ERC1155Receiver implementer"); 323 | } 324 | } 325 | } 326 | 327 | function _isContract(address account) internal view returns (bool) { 328 | return account.code.length > 0; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /contracts/standards/ERC1155_base.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC1155/ERC1155.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 7 | import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; 8 | import "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; 9 | import "@openzeppelin/contracts/utils/Address.sol"; 10 | import "@openzeppelin/contracts/utils/Context.sol"; 11 | import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; 12 | 13 | /** 14 | * @dev Implementation of the basic standard multi-token. 15 | * See https://eips.ethereum.org/EIPS/eip-1155 16 | * Originally based on code by Enjin: https://github.com/enjin/erc-1155 17 | * 18 | * _Available since v3.1._ 19 | */ 20 | contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { 21 | using Address for address; 22 | 23 | // Mapping from token ID to account balances 24 | mapping(uint256 => mapping(address => uint256)) private _balances; 25 | 26 | // Mapping from account to operator approvals 27 | mapping(address => mapping(address => bool)) private _operatorApprovals; 28 | 29 | // Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json 30 | string private _uri; 31 | 32 | /** 33 | * @dev See {_setURI}. 34 | */ 35 | constructor(string memory uri_) { 36 | _setURI(uri_); 37 | } 38 | 39 | /** 40 | * @dev See {IERC165-supportsInterface}. 41 | */ 42 | function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { 43 | return 44 | interfaceId == type(IERC1155).interfaceId || 45 | interfaceId == type(IERC1155MetadataURI).interfaceId || 46 | super.supportsInterface(interfaceId); 47 | } 48 | 49 | /** 50 | * @dev See {IERC1155MetadataURI-uri}. 51 | * 52 | * This implementation returns the same URI for *all* token types. It relies 53 | * on the token type ID substitution mechanism 54 | * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. 55 | * 56 | * Clients calling this function must replace the `\{id\}` substring with the 57 | * actual token type ID. 58 | */ 59 | function uri(uint256) public view virtual override returns (string memory) { 60 | return _uri; 61 | } 62 | 63 | /** 64 | * @dev See {IERC1155-balanceOf}. 65 | * 66 | * Requirements: 67 | * 68 | * - `account` cannot be the zero address. 69 | */ 70 | function balanceOf(address account, uint256 id) public view virtual override returns (uint256) { 71 | require(account != address(0), "ERC1155: address zero is not a valid owner"); 72 | return _balances[id][account]; 73 | } 74 | 75 | /** 76 | * @dev See {IERC1155-balanceOfBatch}. 77 | * 78 | * Requirements: 79 | * 80 | * - `accounts` and `ids` must have the same length. 81 | */ 82 | function balanceOfBatch(address[] memory accounts, uint256[] memory ids) 83 | public 84 | view 85 | virtual 86 | override 87 | returns (uint256[] memory) 88 | { 89 | require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch"); 90 | 91 | uint256[] memory batchBalances = new uint256[](accounts.length); 92 | 93 | for (uint256 i = 0; i < accounts.length; ++i) { 94 | batchBalances[i] = balanceOf(accounts[i], ids[i]); 95 | } 96 | 97 | return batchBalances; 98 | } 99 | 100 | /** 101 | * @dev See {IERC1155-setApprovalForAll}. 102 | */ 103 | function setApprovalForAll(address operator, bool approved) public virtual override { 104 | _setApprovalForAll(_msgSender(), operator, approved); 105 | } 106 | 107 | /** 108 | * @dev See {IERC1155-isApprovedForAll}. 109 | */ 110 | function isApprovedForAll(address account, address operator) public view virtual override returns (bool) { 111 | return _operatorApprovals[account][operator]; 112 | } 113 | 114 | /** 115 | * @dev See {IERC1155-safeTransferFrom}. 116 | */ 117 | function safeTransferFrom( 118 | address from, 119 | address to, 120 | uint256 id, 121 | uint256 amount, 122 | bytes memory data 123 | ) public virtual override { 124 | require( 125 | from == _msgSender() || isApprovedForAll(from, _msgSender()), 126 | "ERC1155: caller is not token owner or approved" 127 | ); 128 | _safeTransferFrom(from, to, id, amount, data); 129 | } 130 | 131 | /** 132 | * @dev See {IERC1155-safeBatchTransferFrom}. 133 | */ 134 | function safeBatchTransferFrom( 135 | address from, 136 | address to, 137 | uint256[] memory ids, 138 | uint256[] memory amounts, 139 | bytes memory data 140 | ) public virtual override { 141 | require( 142 | from == _msgSender() || isApprovedForAll(from, _msgSender()), 143 | "ERC1155: caller is not token owner or approved" 144 | ); 145 | _safeBatchTransferFrom(from, to, ids, amounts, data); 146 | } 147 | 148 | /** 149 | * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. 150 | * 151 | * Emits a {TransferSingle} event. 152 | * 153 | * Requirements: 154 | * 155 | * - `to` cannot be the zero address. 156 | * - `from` must have a balance of tokens of type `id` of at least `amount`. 157 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the 158 | * acceptance magic value. 159 | */ 160 | function _safeTransferFrom( 161 | address from, 162 | address to, 163 | uint256 id, 164 | uint256 amount, 165 | bytes memory data 166 | ) internal virtual { 167 | require(to != address(0), "ERC1155: transfer to the zero address"); 168 | 169 | address operator = _msgSender(); 170 | uint256[] memory ids = _asSingletonArray(id); 171 | uint256[] memory amounts = _asSingletonArray(amount); 172 | 173 | _beforeTokenTransfer(operator, from, to, ids, amounts, data); 174 | 175 | uint256 fromBalance = _balances[id][from]; 176 | require(fromBalance >= amount, "ERC1155: insufficient balance for transfer"); 177 | unchecked { 178 | _balances[id][from] = fromBalance - amount; 179 | } 180 | _balances[id][to] += amount; 181 | 182 | emit TransferSingle(operator, from, to, id, amount); 183 | 184 | _afterTokenTransfer(operator, from, to, ids, amounts, data); 185 | 186 | _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); 187 | } 188 | 189 | /** 190 | * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_safeTransferFrom}. 191 | * 192 | * Emits a {TransferBatch} event. 193 | * 194 | * Requirements: 195 | * 196 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the 197 | * acceptance magic value. 198 | */ 199 | function _safeBatchTransferFrom( 200 | address from, 201 | address to, 202 | uint256[] memory ids, 203 | uint256[] memory amounts, 204 | bytes memory data 205 | ) internal virtual { 206 | require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); 207 | require(to != address(0), "ERC1155: transfer to the zero address"); 208 | 209 | address operator = _msgSender(); 210 | 211 | _beforeTokenTransfer(operator, from, to, ids, amounts, data); 212 | 213 | for (uint256 i = 0; i < ids.length; ++i) { 214 | uint256 id = ids[i]; 215 | uint256 amount = amounts[i]; 216 | 217 | uint256 fromBalance = _balances[id][from]; 218 | require(fromBalance >= amount, "ERC1155: insufficient balance for transfer"); 219 | unchecked { 220 | _balances[id][from] = fromBalance - amount; 221 | } 222 | _balances[id][to] += amount; 223 | } 224 | 225 | emit TransferBatch(operator, from, to, ids, amounts); 226 | 227 | _afterTokenTransfer(operator, from, to, ids, amounts, data); 228 | 229 | _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data); 230 | } 231 | 232 | /** 233 | * @dev Sets a new URI for all token types, by relying on the token type ID 234 | * substitution mechanism 235 | * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. 236 | * 237 | * By this mechanism, any occurrence of the `\{id\}` substring in either the 238 | * URI or any of the amounts in the JSON file at said URI will be replaced by 239 | * clients with the token type ID. 240 | * 241 | * For example, the `https://token-cdn-domain/\{id\}.json` URI would be 242 | * interpreted by clients as 243 | * `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` 244 | * for token type ID 0x4cce0. 245 | * 246 | * See {uri}. 247 | * 248 | * Because these URIs cannot be meaningfully represented by the {URI} event, 249 | * this function emits no events. 250 | */ 251 | function _setURI(string memory newuri) internal virtual { 252 | _uri = newuri; 253 | } 254 | 255 | /** 256 | * @dev Creates `amount` tokens of token type `id`, and assigns them to `to`. 257 | * 258 | * Emits a {TransferSingle} event. 259 | * 260 | * Requirements: 261 | * 262 | * - `to` cannot be the zero address. 263 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the 264 | * acceptance magic value. 265 | */ 266 | function _mint( 267 | address to, 268 | uint256 id, 269 | uint256 amount, 270 | bytes memory data 271 | ) internal virtual { 272 | require(to != address(0), "ERC1155: mint to the zero address"); 273 | 274 | address operator = _msgSender(); 275 | uint256[] memory ids = _asSingletonArray(id); 276 | uint256[] memory amounts = _asSingletonArray(amount); 277 | 278 | _beforeTokenTransfer(operator, address(0), to, ids, amounts, data); 279 | 280 | _balances[id][to] += amount; 281 | emit TransferSingle(operator, address(0), to, id, amount); 282 | 283 | _afterTokenTransfer(operator, address(0), to, ids, amounts, data); 284 | 285 | _doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data); 286 | } 287 | 288 | /** 289 | * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_mint}. 290 | * 291 | * Emits a {TransferBatch} event. 292 | * 293 | * Requirements: 294 | * 295 | * - `ids` and `amounts` must have the same length. 296 | * - If `to` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155BatchReceived} and return the 297 | * acceptance magic value. 298 | */ 299 | function _mintBatch( 300 | address to, 301 | uint256[] memory ids, 302 | uint256[] memory amounts, 303 | bytes memory data 304 | ) internal virtual { 305 | require(to != address(0), "ERC1155: mint to the zero address"); 306 | require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); 307 | 308 | address operator = _msgSender(); 309 | 310 | _beforeTokenTransfer(operator, address(0), to, ids, amounts, data); 311 | 312 | for (uint256 i = 0; i < ids.length; i++) { 313 | _balances[ids[i]][to] += amounts[i]; 314 | } 315 | 316 | emit TransferBatch(operator, address(0), to, ids, amounts); 317 | 318 | _afterTokenTransfer(operator, address(0), to, ids, amounts, data); 319 | 320 | _doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data); 321 | } 322 | 323 | /** 324 | * @dev Destroys `amount` tokens of token type `id` from `from` 325 | * 326 | * Emits a {TransferSingle} event. 327 | * 328 | * Requirements: 329 | * 330 | * - `from` cannot be the zero address. 331 | * - `from` must have at least `amount` tokens of token type `id`. 332 | */ 333 | function _burn( 334 | address from, 335 | uint256 id, 336 | uint256 amount 337 | ) internal virtual { 338 | require(from != address(0), "ERC1155: burn from the zero address"); 339 | 340 | address operator = _msgSender(); 341 | uint256[] memory ids = _asSingletonArray(id); 342 | uint256[] memory amounts = _asSingletonArray(amount); 343 | 344 | _beforeTokenTransfer(operator, from, address(0), ids, amounts, ""); 345 | 346 | uint256 fromBalance = _balances[id][from]; 347 | require(fromBalance >= amount, "ERC1155: burn amount exceeds balance"); 348 | unchecked { 349 | _balances[id][from] = fromBalance - amount; 350 | } 351 | 352 | emit TransferSingle(operator, from, address(0), id, amount); 353 | 354 | _afterTokenTransfer(operator, from, address(0), ids, amounts, ""); 355 | } 356 | 357 | /** 358 | * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {_burn}. 359 | * 360 | * Emits a {TransferBatch} event. 361 | * 362 | * Requirements: 363 | * 364 | * - `ids` and `amounts` must have the same length. 365 | */ 366 | function _burnBatch( 367 | address from, 368 | uint256[] memory ids, 369 | uint256[] memory amounts 370 | ) internal virtual { 371 | require(from != address(0), "ERC1155: burn from the zero address"); 372 | require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); 373 | 374 | address operator = _msgSender(); 375 | 376 | _beforeTokenTransfer(operator, from, address(0), ids, amounts, ""); 377 | 378 | for (uint256 i = 0; i < ids.length; i++) { 379 | uint256 id = ids[i]; 380 | uint256 amount = amounts[i]; 381 | 382 | uint256 fromBalance = _balances[id][from]; 383 | require(fromBalance >= amount, "ERC1155: burn amount exceeds balance"); 384 | unchecked { 385 | _balances[id][from] = fromBalance - amount; 386 | } 387 | } 388 | 389 | emit TransferBatch(operator, from, address(0), ids, amounts); 390 | 391 | _afterTokenTransfer(operator, from, address(0), ids, amounts, ""); 392 | } 393 | 394 | /** 395 | * @dev Approve `operator` to operate on all of `owner` tokens 396 | * 397 | * Emits an {ApprovalForAll} event. 398 | */ 399 | function _setApprovalForAll( 400 | address owner, 401 | address operator, 402 | bool approved 403 | ) internal virtual { 404 | require(owner != operator, "ERC1155: setting approval status for self"); 405 | _operatorApprovals[owner][operator] = approved; 406 | emit ApprovalForAll(owner, operator, approved); 407 | } 408 | 409 | /** 410 | * @dev Hook that is called before any token transfer. This includes minting 411 | * and burning, as well as batched variants. 412 | * 413 | * The same hook is called on both single and batched variants. For single 414 | * transfers, the length of the `ids` and `amounts` arrays will be 1. 415 | * 416 | * Calling conditions (for each `id` and `amount` pair): 417 | * 418 | * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens 419 | * of token type `id` will be transferred to `to`. 420 | * - When `from` is zero, `amount` tokens of token type `id` will be minted 421 | * for `to`. 422 | * - when `to` is zero, `amount` of ``from``'s tokens of token type `id` 423 | * will be burned. 424 | * - `from` and `to` are never both zero. 425 | * - `ids` and `amounts` have the same, non-zero length. 426 | * 427 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 428 | */ 429 | function _beforeTokenTransfer( 430 | address operator, 431 | address from, 432 | address to, 433 | uint256[] memory ids, 434 | uint256[] memory amounts, 435 | bytes memory data 436 | ) internal virtual {} 437 | 438 | /** 439 | * @dev Hook that is called after any token transfer. This includes minting 440 | * and burning, as well as batched variants. 441 | * 442 | * The same hook is called on both single and batched variants. For single 443 | * transfers, the length of the `id` and `amount` arrays will be 1. 444 | * 445 | * Calling conditions (for each `id` and `amount` pair): 446 | * 447 | * - When `from` and `to` are both non-zero, `amount` of ``from``'s tokens 448 | * of token type `id` will be transferred to `to`. 449 | * - When `from` is zero, `amount` tokens of token type `id` will be minted 450 | * for `to`. 451 | * - when `to` is zero, `amount` of ``from``'s tokens of token type `id` 452 | * will be burned. 453 | * - `from` and `to` are never both zero. 454 | * - `ids` and `amounts` have the same, non-zero length. 455 | * 456 | * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. 457 | */ 458 | function _afterTokenTransfer( 459 | address operator, 460 | address from, 461 | address to, 462 | uint256[] memory ids, 463 | uint256[] memory amounts, 464 | bytes memory data 465 | ) internal virtual {} 466 | 467 | function _doSafeTransferAcceptanceCheck( 468 | address operator, 469 | address from, 470 | address to, 471 | uint256 id, 472 | uint256 amount, 473 | bytes memory data 474 | ) private { 475 | if (to.isContract()) { 476 | try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) { 477 | if (response != IERC1155Receiver.onERC1155Received.selector) { 478 | revert("ERC1155: ERC1155Receiver rejected tokens"); 479 | } 480 | } catch Error(string memory reason) { 481 | revert(reason); 482 | } catch { 483 | revert("ERC1155: transfer to non-ERC1155Receiver implementer"); 484 | } 485 | } 486 | } 487 | 488 | function _doSafeBatchTransferAcceptanceCheck( 489 | address operator, 490 | address from, 491 | address to, 492 | uint256[] memory ids, 493 | uint256[] memory amounts, 494 | bytes memory data 495 | ) private { 496 | if (to.isContract()) { 497 | try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns ( 498 | bytes4 response 499 | ) { 500 | if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { 501 | revert("ERC1155: ERC1155Receiver rejected tokens"); 502 | } 503 | } catch Error(string memory reason) { 504 | revert(reason); 505 | } catch { 506 | revert("ERC1155: transfer to non-ERC1155Receiver implementer"); 507 | } 508 | } 509 | } 510 | 511 | function _asSingletonArray(uint256 element) private pure returns (uint256[] memory) { 512 | uint256[] memory array = new uint256[](1); 513 | array[0] = element; 514 | 515 | return array; 516 | } 517 | } 518 | -------------------------------------------------------------------------------- /contracts/test/ReceiveBlock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | contract ReceiveBlock { 5 | error FailedProxySend(); 6 | 7 | /// @notice Block incoming ETH. 8 | receive() external payable { 9 | revert(); 10 | } 11 | 12 | function send(address recipient) external payable { 13 | (bool success,) = recipient.call{ value: msg.value }(""); 14 | 15 | if (! success) revert FailedProxySend(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /data/GENESIS_0_RECIPIENTS.json: -------------------------------------------------------------------------------- 1 | [ 2 | "0x44a3ccddccae339d05200a8f4347f83a58847e52", 3 | "0xc31328e68ba9770d4c3375fd2b7c79c9904c711f", 4 | "0xc3c7ea560b5533da8eee31212a9595a419e6d631", 5 | "0x97b439db483bb0e02c709b2e948a32ee72daa82d", 6 | "0xb3b5102aee1557135449ec0d514f2b7334769af2", 7 | "0x184a973a0ed40b7c4ee187b844bd1455f2191a1d", 8 | "0x7637dcb54a019a027175964ad845763d3fc3b5cd", 9 | "0x78b05cc4f278662416083dfe211f7496abc38518", 10 | "0x69e68074f1aada957edd39c5eae0069973343f30", 11 | "0x4b5fe817488d86f55ab1a4e40598a3cfbde95b6a", 12 | "0x1a1427a73b7cb0f4ea3f71c6c8090c4366c8ebe1", 13 | "0x2177da04f9479496c2292d6344d306aa49beb34a", 14 | "0xf0d6999725115e3ead3d927eb3329d63afaec09b", 15 | "0xe11da9560b51f8918295edc5ab9c0a90e9ada20b", 16 | "0x047d0cb513a2ccb1ed44d4af271410a9c8d5248f", 17 | "0x12c29df62b1b254e1311bef777a37bf8c575b58d", 18 | "0x16a2ebd556af2a39eba349be7d6eba7a57c47711", 19 | "0x1da5331994e781ab0e2af9f85bfce2037a514170", 20 | "0x1f8615a2caa4c8ab08bf1312e022341022c90e37", 21 | "0x243db4aaa62b3785df27c364090749b96e45f3ec", 22 | "0x324111baf9886e0456d74cef0ecbd49ed0779ee1", 23 | "0x343e73d33639d9c354fa215ca23ffcd0f5604ac1", 24 | "0x367dc97068ab54ba1dfbfc0fad12fbcb7b3a0d09", 25 | "0x3c1bee74ff0d159ab06b6113417fc536499057c6", 26 | "0x461a5b8326ba0e2dfd133651a3b559dc8d3b0400", 27 | "0x4f55fd3b7ec169a364e34eb7f523ca5ceb12c888", 28 | "0x51787a2c56d710c68140bdadefd3a98bff96feb4", 29 | "0x59590178e9eddb027ed87bed9c1da8cd5129fed9", 30 | "0x5efdb6d8c798c2c2bea5b1961982a5944f92a5c1", 31 | "0x5f45c7ea2e094fea813a8a8813620ffcc4a19d0f", 32 | "0x626ffae9f5537d4fadb6065585213f095b106bfc", 33 | "0x65a4c69f4ea3fea89a0d4156c3d91c787472b670", 34 | "0x67164d8853d3dc9ab5d11cb6744e6f04401cf772", 35 | "0x679d5162bad71990abca0f18095948c12a2756b0", 36 | "0x721fe9de376bd927bb15fd25de41bf3d420b8c07", 37 | "0x72915ad3110eb31768a562f540ac1ebcd51d3dc8", 38 | "0x75432062a9bcc6bc5c294f44a8e3aa65bec8a64d", 39 | "0x7a3cf7122eb2e3f3820f0afaaac4206cdc50bd7e", 40 | "0x81eef2d7c4033c3224660b6a5d4b4a4727f1c762", 41 | "0x881475210e75b814d5b711090a064942b6f30605", 42 | "0xa3e0f18fec11b027bc23fd8a1cac729cffab11d9", 43 | "0xb69cd25391df0b18e6cafe9dd61b966388d6beec", 44 | "0xc0dcce9d4f0e5dbf20dc2631786550dbdeebf756", 45 | "0xc3d1b0445c7d62fdb86e02c467f4239478b37f20", 46 | "0xc8f8e2f59dd95ff67c3d39109eca2e2a017d4c8a", 47 | "0xd52fba65a52214a65c78dc7f95c9fee1f13a5955", 48 | "0xe2601273be4c89a729a3fe0af4bd6503bb20c27c", 49 | "0xe3603cf313dcd49cc3061c9850a474d40aa81159", 50 | "0xe5b0a257f0420ed4860d5d431e6ad6744da08405", 51 | "0xf035af8a3dff1f90aeb21f9623589ae1330e9e09", 52 | "0xfc8f57a04f4df6f167599162bf5fdaa098ac18cc", 53 | "0xdbefee517025559e7898d3a48f18221c32d3fcf5", 54 | "0xe7967e0ec15cb48939fcf0bc5764c2a634349ecb", 55 | "0xd4a08cf067c83d1b2cc1d26831569b7850804be7", 56 | "0x3025430ae8a81cd13e3d0969b1093f8d82bbbd7d", 57 | "0x4f7527a5abfd4c946cb1df410898b0f1fb3ab1e3", 58 | "0x0771afe2574c2863b310b3c14e1fc8e33dcf5395", 59 | "0x55d07dcf9dee64e14b3aa7269f8c9e950cf7f2b9", 60 | "0x962a7d642178c87eeec92240669ec98f868dc54e", 61 | "0xa5af072b89525ef22b60bb1aa2107a48f35d0cfe", 62 | "0x668891911ffb50a1e5c20e2e9f985ed9fa8f05e3", 63 | "0xdf31865ce328705e08912b7067a45933427e1ac2", 64 | "0x5d802e2fe48392c104ce0401c7eca8a4456f1f16", 65 | "0xaec4075e4fc8ce829bc4f0cfd5e2fa10dd2b12d1", 66 | "0x463fc06d16e146ec9e4cb2ab8c3077732c75f38b", 67 | "0x71810b26185b7b820ba586e78686ce38619d711f", 68 | "0x8f54249ae4c8a73e92d44459e026c9197670f3fd", 69 | "0x8162890aaa7c3f9147da2489025ee27ea43ce6fd", 70 | "0x81590c16b9d1495d133cf881acf5cc0b56730b74", 71 | "0x77acac99ac831a1574b9db4d15299e98e195e6ae", 72 | "0x7b75bc70b928472856047fdef0d08d5b5816aefd", 73 | "0x1aae1bc09e1785c8cb00650db0c6015bd73b0c7e", 74 | "0x7494308fb7cc000dcae7b30317b9401bfc1f36bb", 75 | "0xc3f1d077fa590018d3924ce8da1a085b4eae506d", 76 | "0x8715ed9bbd4f8cfbed14f045556f19f39f11c75b", 77 | "0xd5a06056701c7cfba12b1a384294148b9d2d8bca", 78 | "0x9027bd5b0d32b0cb51dce93e8add64aca3a24912", 79 | "0x99b8226ec774c957ffe2747fdfbae8f22741e899", 80 | "0x9c81559aac49da60ff1e7755436dc2924d1a4c4f", 81 | "0x36ed861a319278e5b6c39028884b0ca93df105d0", 82 | "0x5d1ed72965bc8abd5aaea2322098fb926186d62b", 83 | "0x7ba8a12786febf3a4a49b2659c23dcad09690895", 84 | "0x083090dd278b599b89e771f11fe9f7d4f4418918", 85 | "0x64f9de27e3878e59f8a15a1b1da61aca5b317ec0", 86 | "0xd5917a0a13bbc615c5ad4553266fd9373b5c7283", 87 | "0xb2d2ecc7d94cfb8e70f60aeb97bf7f4c4cb8ef28", 88 | "0xc2322d5e0cb1dc57695ecae84e5f6868c17f1cb7", 89 | "0xfd26dd161d1df11c526adff5daa6380bef8b7564", 90 | "0xb379b56bcacdd58ae0768654763881849bfaad94", 91 | "0xd2dea18d040152c580f29195b29670633b0c9796", 92 | "0x36041d9f21901599b9825bf4ecdf03aee81be0a6", 93 | "0x92890034a3dcb1c38c1184775788ba0f95c23f03", 94 | "0xcd64a3e77335b652f8582e081f02bf9c90f908a4", 95 | "0x3348de5cc020eefa5d3b8212aefbc0c19d315f24", 96 | "0xd8208bbf403a5de65a61e45321580fd0e31d4432", 97 | "0xf6e9ccea1f646fee85e13ab3361c75959a94d5b0", 98 | "0x97a6d7365e64d56b8a9aede0096a0fa2ace4eebc", 99 | "0x9adccfd429a7ba40e7945841164ea2cffa7611fd", 100 | "0x3689c216f8f6ce7e2ce2a27c81a23096a787f532", 101 | "0xd2498b4dc8402789736f7c94caf969ea65badfa2", 102 | "0xb079622613d190de0f9ed29b4720ada3372b6f29", 103 | "0x13e2a8e80fd44de5e2a60246293654700e7214de", 104 | "0x0d0e328b8fbc4a148eeae9e7b4791d7a6a0d2d07", 105 | "0xf56345338cb4cddaf915ebef3bfde63e70fe3053" 106 | ] 107 | -------------------------------------------------------------------------------- /deploy/001-infinity.ts: -------------------------------------------------------------------------------- 1 | import {HardhatRuntimeEnvironment} from 'hardhat/types'; 2 | import {DeployFunction} from 'hardhat-deploy/types'; 3 | import GENESIS_RECIPIENTS from '../data/GENESIS_0_RECIPIENTS.json' 4 | import { parseEther } from 'ethers'; 5 | 6 | const PRICE = parseEther('0.008') 7 | 8 | export const deployWithLibraries = async ( 9 | hre: HardhatRuntimeEnvironment, 10 | salt: string = '0x3997c69a39dd451b5503e35287918552c9384b529b80b77476919bfe2def4f36' 11 | ) => { 12 | const {deployments, getNamedAccounts} = hre; 13 | const {deploy} = deployments; 14 | 15 | const {deployer} = await getNamedAccounts(); 16 | 17 | const { address: utilitiesAddress } = await deploy('Utilities', { 18 | from: deployer, 19 | args: [], 20 | log: true, 21 | autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks 22 | deterministicDeployment: '0x00', 23 | }); 24 | 25 | const { address: infiniteGeneratorAddress } = await deploy('InfiniteGenerator', { 26 | from: deployer, 27 | args: [], 28 | libraries: { 29 | Utilities: utilitiesAddress, 30 | }, 31 | log: true, 32 | autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks 33 | deterministicDeployment: '0x00', 34 | }) 35 | 36 | const { address: infiniteArtAddress } = await deploy('InfiniteArt', { 37 | from: deployer, 38 | args: [], 39 | libraries: { 40 | Utilities: utilitiesAddress, 41 | }, 42 | log: true, 43 | autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks 44 | deterministicDeployment: '0x00', 45 | }) 46 | 47 | const { address: infiniteMetadataAddress } = await deploy('InfiniteMetadata', { 48 | from: deployer, 49 | args: [], 50 | libraries: { 51 | Utilities: utilitiesAddress, 52 | InfiniteArt: infiniteArtAddress, 53 | }, 54 | log: true, 55 | autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks 56 | deterministicDeployment: '0x00', 57 | }) 58 | 59 | const { address: infinityAddress } = await deploy('Infinity', { 60 | value: (PRICE * BigInt(GENESIS_RECIPIENTS.length)).toString(), 61 | from: deployer, 62 | args: [GENESIS_RECIPIENTS], 63 | libraries: { 64 | InfiniteGenerator: infiniteGeneratorAddress, 65 | InfiniteArt: infiniteArtAddress, 66 | InfiniteMetadata: infiniteMetadataAddress, 67 | }, 68 | log: true, 69 | autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks 70 | deterministicDeployment: salt, 71 | }) 72 | 73 | return { 74 | utilitiesAddress, 75 | infiniteGeneratorAddress, 76 | infiniteArtAddress, 77 | infiniteMetadataAddress, 78 | infinityAddress, 79 | } 80 | } 81 | 82 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 83 | await deployWithLibraries(hre) 84 | }; 85 | 86 | export default func; 87 | 88 | func.tags = ['Infinity']; 89 | -------------------------------------------------------------------------------- /deploy/002-batchsend.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { DeployFunction } from 'hardhat-deploy/types'; 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const { deployments, getNamedAccounts } = hre; 6 | const { deploy } = deployments; 7 | 8 | const { deployer } = await getNamedAccounts(); 9 | 10 | await deploy('BatchSend', { 11 | from: deployer, 12 | args: [], 13 | log: true, 14 | autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks 15 | }); 16 | }; 17 | 18 | export default func; 19 | 20 | func.tags = ['BatchSend']; 21 | -------------------------------------------------------------------------------- /deploy/003-mocks.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types'; 2 | import { DeployFunction } from 'hardhat-deploy/types'; 3 | 4 | const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { 5 | const { deployments, getNamedAccounts } = hre; 6 | const { deploy } = deployments; 7 | 8 | const { deployer } = await getNamedAccounts(); 9 | 10 | await deploy('ReceiveBlock', { 11 | from: deployer, 12 | args: [], 13 | log: true, 14 | autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks 15 | }); 16 | }; 17 | 18 | export default func; 19 | 20 | func.tags = ['Mocks']; 21 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | 3 | import "@nomicfoundation/hardhat-toolbox" 4 | import "hardhat-contract-sizer" 5 | import 'hardhat-deploy' 6 | 7 | import './tasks/refunds' 8 | import './tasks/airdrop' 9 | import './tasks/find-seed' 10 | 11 | dotenv.config() 12 | 13 | const HARDHAT_NETWORK_CONFIG = { 14 | chainId: 1337, 15 | forking: { 16 | url: process.env.MAINNET_URL || '', 17 | blockNumber: 17593000, 18 | }, 19 | allowUnlimitedContractSize: true, 20 | } 21 | 22 | const config = { 23 | solidity: { 24 | version: "0.8.20", 25 | settings: { 26 | optimizer: { 27 | enabled: true, 28 | runs: 1000, 29 | }, 30 | }, 31 | }, 32 | namedAccounts: { 33 | deployer: { 34 | default: 0, // first account as deployer 35 | }, 36 | vv: { 37 | default: '0xc8f8e2F59Dd95fF67c3d39109ecA2e2A017D4c8a' 38 | }, 39 | }, 40 | networks: { 41 | mainnet: { 42 | url: process.env.MAINNET_URL || '', 43 | accounts: 44 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], 45 | }, 46 | goerli: { 47 | url: process.env.GOERLI_URL || '', 48 | accounts: 49 | process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], 50 | }, 51 | localhost: { 52 | ...HARDHAT_NETWORK_CONFIG, 53 | }, 54 | hardhat: HARDHAT_NETWORK_CONFIG, 55 | }, 56 | gasReporter: { 57 | enabled: process.env.REPORT_GAS !== undefined, 58 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 59 | currency: 'USD', 60 | excludeContracts: [ 61 | '@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:ERC721Upgradeable', 62 | ] 63 | }, 64 | etherscan: { 65 | apiKey: process.env.ETHERSCAN_API_KEY, 66 | }, 67 | mocha: { 68 | timeout: 120_000_000, 69 | }, 70 | } 71 | 72 | export default config 73 | -------------------------------------------------------------------------------- /helpers/arrays.ts: -------------------------------------------------------------------------------- 1 | export function chunk(arr: T[], size: number): T[][] { 2 | return Array.from( 3 | { length: Math.ceil(arr.length / size) }, 4 | (_: any, i: number) => arr.slice(i * size, i * size + size) 5 | ) 6 | } 7 | -------------------------------------------------------------------------------- /helpers/constants.ts: -------------------------------------------------------------------------------- 1 | export const VV = '0xc8f8e2F59Dd95fF67c3d39109ecA2e2A017D4c8a' 2 | export const JACK = '0xD1295FcBAf56BF1a6DFF3e1DF7e437f987f6feCa' 3 | export const JALIL = '0xe11Da9560b51f8918295edC5ab9c0a90E9ADa20B' 4 | export const JALIL_VAULT = '0x721fE9dE376bD927BB15Fd25DE41Bf3d420b8c07' 5 | -------------------------------------------------------------------------------- /helpers/decode-uri.ts: -------------------------------------------------------------------------------- 1 | export const decodeBase64URI = (uri: string) => { 2 | const json = Buffer.from(uri.substring(29), 'base64').toString() 3 | return JSON.parse(json) 4 | } 5 | -------------------------------------------------------------------------------- /helpers/impersonate.ts: -------------------------------------------------------------------------------- 1 | import { HardhatRuntimeEnvironment } from 'hardhat/types' 2 | import { setBalance } from '@nomicfoundation/hardhat-network-helpers' 3 | 4 | export const impersonate = async (address: string, hre: HardhatRuntimeEnvironment) => { 5 | await hre.network.provider.request({ 6 | method: 'hardhat_impersonateAccount', 7 | params: [address], 8 | }) 9 | 10 | setBalance(address, 100n ** 18n) 11 | 12 | return await hre.ethers.getSigner(address) 13 | } 14 | -------------------------------------------------------------------------------- /helpers/render-pngs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import puppeteer, { Browser, Page } from 'puppeteer' 3 | 4 | let browser: Browser 5 | let page: Page 6 | 7 | const setupPNGRenderer = async () => { 8 | browser = await puppeteer.launch({ headless: 'new' }) 9 | page = await browser.newPage() 10 | await page.setViewport({ 11 | width: 1400, 12 | height: 1400, 13 | deviceScaleFactor: 1, 14 | }) 15 | } 16 | 17 | const shutdownPNGRenderer = async () => browser.close() 18 | 19 | const renderPNG = async (svg: string, path: string) => { 20 | await page.setContent(svg) 21 | await page.addStyleTag({ content: 'body { margin: 0; }' }) 22 | await page.waitForSelector('svg') 23 | 24 | await page.screenshot({ 25 | path 26 | }) 27 | 28 | console.log(`Rendered ${path}`) 29 | } 30 | 31 | export const render = async (path: string) => { 32 | await setupPNGRenderer() 33 | try { fs.rmdirSync(`${path}/pngs`, { recursive: true }) } catch (e) {} 34 | try { fs.mkdirSync(`${path}/pngs`, { recursive: true }) } catch (e) {} 35 | 36 | const files = fs.readdirSync(`${path}/`).filter(f => f.indexOf('.svg') > -1) 37 | 38 | for (const file of files) { 39 | const svg = fs.readFileSync(`${path}/${file}`).toString() 40 | 41 | await renderPNG(svg, `${path}/pngs/${file.replace('svg', 'png')}`) 42 | } 43 | 44 | await shutdownPNGRenderer() 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "name": "infinities-contract", 4 | "devDependencies": { 5 | "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", 6 | "@nomicfoundation/hardhat-ethers": "^3.0.4", 7 | "@nomicfoundation/hardhat-network-helpers": "^1.0.8", 8 | "@nomicfoundation/hardhat-toolbox": "^3.0.0", 9 | "@nomicfoundation/hardhat-verify": "^1.1.1", 10 | "@openzeppelin/contracts": "^4.9.3", 11 | "@typechain/ethers-v6": "^0.4.3", 12 | "@typechain/hardhat": "^8.0.3", 13 | "@types/mocha": "^10.0.1", 14 | "@typescript-eslint/eslint-plugin": "^6.4.0", 15 | "@typescript-eslint/parser": "^6.4.0", 16 | "chai": "^4.3.7", 17 | "dotenv": "^10.0.0", 18 | "eslint": "^7.32.0", 19 | "eslint-config-prettier": "^8.8.0", 20 | "eslint-config-standard": "^16.0.3", 21 | "eslint-plugin-import": "^2.27.5", 22 | "eslint-plugin-node": "^11.1.0", 23 | "eslint-plugin-prettier": "^3.4.1", 24 | "eslint-plugin-promise": "^5.2.0", 25 | "ethers": "^6.7.0", 26 | "hardhat": "^2.17.1", 27 | "hardhat-contract-sizer": "^2.10.0", 28 | "hardhat-deploy": "^0.11.34", 29 | "hardhat-deploy-ethers": "^0.4.1", 30 | "hardhat-gas-reporter": "^1.0.9", 31 | "prettier": "^2.8.8", 32 | "prettier-plugin-solidity": "^1.1.3", 33 | "puppeteer": "^20.7.4", 34 | "puppeteer-screen-recorder": "^2.1.2", 35 | "solhint": "^3.4.1", 36 | "solidity-coverage": "^0.8.4", 37 | "ts-node": "^10.9.1", 38 | "typechain": "^8.3.1", 39 | "typescript": "^4.9.5" 40 | }, 41 | "packageManager": "yarn@3.6.2" 42 | } 43 | -------------------------------------------------------------------------------- /tasks/airdrop.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config' 2 | import { ZeroAddress, parseEther } from 'ethers' 3 | 4 | import CLAIMS from './../data/infinity-claims.json' 5 | import { chunk } from '../helpers/arrays' 6 | import { impersonate } from '../helpers/impersonate' 7 | import { VV } from '../helpers/constants' 8 | 9 | const PRICE = parseEther('0.008') 10 | 11 | task('airdrop-claimers', 'Airdrop claimers') 12 | .addParam('address') 13 | .setAction(async ({ address }, hre) => { 14 | const chunks = chunk(CLAIMS, 646) 15 | 16 | const {getNamedAccounts} = hre; 17 | 18 | // // TODO: Replace below two lines... 19 | // const { deployer } = await getNamedAccounts(); 20 | // const signer = await hre.ethers.getSigner(deployer) 21 | const vv = await impersonate(VV, hre) 22 | const signer = vv 23 | 24 | 25 | const contract = await hre.ethers.getContractAt('Infinity', address, signer) 26 | 27 | for (const chunk of chunks) { 28 | const sources = chunk.map(_ => ZeroAddress) 29 | const recipients = chunk.map(r => r.claimer) 30 | const tokens = chunk.map(r => r.token_id) 31 | const amounts = chunk.map(_ => 1) 32 | 33 | const value = PRICE * BigInt(chunk.length) 34 | 35 | await contract.connect(signer).generateManyExisting(sources, recipients, tokens, amounts, { 36 | value, 37 | }) 38 | } 39 | 40 | }) 41 | -------------------------------------------------------------------------------- /tasks/find-seed.ts: -------------------------------------------------------------------------------- 1 | import { task } from 'hardhat/config' 2 | import { AbiCoder, getCreate2Address, id, keccak256, toBeHex } from 'ethers' 3 | import { deployWithLibraries } from './../deploy/001-infinity' 4 | import GENESIS_RECIPIENTS from './../data/GENESIS_0_RECIPIENTS.json' 5 | 6 | async function findSaltForAddress( 7 | desiredPrefix: string, 8 | desiredSuffix: string, 9 | seedOffset: string, 10 | initCodeHash: string, 11 | ): Promise { 12 | const deployer = '0x4e59b44847b379578588920cA78FbF26c0B4956C' 13 | const startSeed = BigInt(id(seedOffset)) 14 | console.log(`Using start seed ${seedOffset} (${startSeed})`) 15 | 16 | let salt = 1n 17 | while (true) { 18 | const saltHex = toBeHex(salt + startSeed, 32) 19 | 20 | const address = getCreate2Address(deployer, saltHex, initCodeHash) 21 | 22 | const hasCorrectPrefix = address.toLowerCase().startsWith(desiredPrefix) 23 | const hasCorrectSuffix = address.slice(42 - desiredSuffix.length) == desiredSuffix 24 | if (hasCorrectPrefix && hasCorrectSuffix) { 25 | return [saltHex, address] 26 | } 27 | salt++ 28 | if (salt % 1_000_000n === 0n) { 29 | console.log(`${salt} tries so far`) 30 | } 31 | } 32 | } 33 | 34 | task('find-salt', 'Find salt for desired contract address prefix') 35 | .addParam('prefix', 'The desired address prefix') 36 | .addParam('suffix', 'The desired address suffix') 37 | .addParam('seed', 'Initial seed') 38 | .setAction(async (args, hre) => { 39 | const { ethers } = hre 40 | 41 | const { 42 | infiniteGeneratorAddress, 43 | infiniteArtAddress, 44 | infiniteMetadataAddress, 45 | } = await deployWithLibraries(hre) 46 | 47 | const factory = await ethers.getContractFactory('Infinity', { 48 | libraries: { 49 | InfiniteGenerator: infiniteGeneratorAddress, 50 | InfiniteArt: infiniteArtAddress, 51 | InfiniteMetadata: infiniteMetadataAddress, 52 | }, 53 | }) 54 | 55 | const constructorArguments = AbiCoder.defaultAbiCoder() 56 | .encode(['address[]'], [GENESIS_RECIPIENTS]) 57 | .slice(2) // strip 0x 58 | 59 | const initCode = factory.bytecode + constructorArguments 60 | const initCodeHash = keccak256(initCode) 61 | 62 | console.log(`Searching for salt to get address with prefix ${args.prefix} and suffix ${args.suffix}...`) 63 | const [salt, address] = await findSaltForAddress(args.prefix, args.suffix, args.seed, initCodeHash) 64 | console.log(`Found salt: ${salt}; final address: ${address}`) 65 | }) 66 | -------------------------------------------------------------------------------- /tasks/refunds.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { task } from 'hardhat/config' 3 | import { formatEther, parseEther } from 'ethers' 4 | 5 | import HOLDINGS from './../data/infinities-holdings.json' 6 | import GAS_SPEND from './../data/infinities-gas-spent.json' 7 | import { chunk } from '../helpers/arrays' 8 | 9 | const PRICE = parseEther('0.008') 10 | 11 | task('refund-test', 'Refund token holdings and gas spent') 12 | .addParam('address') 13 | .setAction(async ({ address }, hre) => { 14 | 15 | const {getNamedAccounts} = hre; 16 | const { deployer } = await getNamedAccounts(); 17 | const signer = await hre.ethers.getSigner(deployer) 18 | const contract = await hre.ethers.getContractAt('BatchSend', address, signer) 19 | 20 | const addresses = ['0xD1295FcBAf56BF1a6DFF3e1DF7e437f987f6feCa', '0xe11Da9560b51f8918295edC5ab9c0a90E9ADa20B'] 21 | const amounts = ['2', '1'] 22 | 23 | const value = amounts.reduce((amount, a) => amount + BigInt(a), 0n) 24 | 25 | await contract.send(addresses, amounts, { 26 | value, 27 | }) 28 | 29 | }) 30 | 31 | task('refund', 'Refund token holdings and gas spent') 32 | .addParam('address') 33 | .setAction(async ({ address }, hre) => { 34 | const accounts: { [key: string]: any } = {} 35 | 36 | for (const account of HOLDINGS) { 37 | accounts[account.address] = account 38 | } 39 | 40 | for (const account of GAS_SPEND) { 41 | if (!accounts[account.address]) { 42 | continue 43 | } 44 | 45 | accounts[account.address] = { 46 | ...accounts[account.address], 47 | ...account, 48 | } 49 | } 50 | 51 | const refunds = Object.values(accounts).map(a => { 52 | const valueHeld = PRICE * BigInt(a.amount) 53 | const totalRefund = valueHeld + BigInt(a.totalfee || 0) 54 | 55 | return { 56 | ...a, 57 | totalFeeInEth: formatEther(a.totalfee || 0), 58 | valueHeld: valueHeld.toString(), 59 | valueHeldEth: formatEther(valueHeld), 60 | totalRefund: totalRefund.toString(), 61 | totalRefundEth: formatEther(totalRefund), 62 | } 63 | }) 64 | 65 | const totalRefunds = refunds.reduce((amount, r) => amount + BigInt(r.totalRefund), 0n) 66 | console.log(formatEther(totalRefunds), 'ether') 67 | 68 | fs.writeFileSync(`data/refunds.json`, JSON.stringify(refunds, null, 4)) 69 | 70 | const chunks = chunk(refunds, 1500) 71 | 72 | const {getNamedAccounts} = hre; 73 | const { deployer } = await getNamedAccounts(); 74 | const signer = await hre.ethers.getSigner(deployer) 75 | const contract = await hre.ethers.getContractAt('BatchSend', address, signer) 76 | 77 | for (const chunk of chunks) { 78 | const addresses = chunk.map(r => r.address) 79 | const amounts = chunk.map(r => r.totalRefund) 80 | 81 | const value = amounts.reduce((amount, a) => amount + BigInt(a), 0n) 82 | 83 | await contract.send(addresses, amounts, { 84 | value, 85 | }) 86 | } 87 | 88 | }) 89 | -------------------------------------------------------------------------------- /test/Infinity.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { expect } from 'chai' 3 | import hre, { deployments } from 'hardhat' 4 | import '@nomicfoundation/hardhat-chai-matchers' 5 | import { ZeroAddress, toBeArray, parseEther, ContractTransactionReceipt, BaseContract, LogDescription } from 'ethers' 6 | import { impersonate } from './../helpers/impersonate' 7 | import { decodeBase64URI } from '../helpers/decode-uri' 8 | import { VV, JALIL } from '../helpers/constants' 9 | import { render } from '../helpers/render-pngs' 10 | import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers' 11 | import { Infinity, ReceiveBlock } from '../typechain-types' 12 | 13 | const PRICE = parseEther('0.008') 14 | 15 | export const deployContract = deployments.createFixture(async ({deployments, ethers}) => { 16 | await deployments.fixture(['Infinity', 'Mocks']) 17 | 18 | const Infinity = await deployments.get('Infinity') 19 | const contract = await ethers.getContractAt('Infinity', Infinity.address) 20 | 21 | const ReceiveBlock = await deployments.get('ReceiveBlock') 22 | const receiveBlockContract = await ethers.getContractAt('ReceiveBlock', ReceiveBlock.address) 23 | 24 | const [ owner, addr1, addr2, addr3, addr4, addr5 ] = await ethers.getSigners() 25 | const vv = await impersonate(VV, hre) 26 | const jalil = await impersonate(JALIL, hre) 27 | 28 | return { contract, owner, addr1, addr2, addr3, addr4, addr5, vv, jalil, receiveBlockContract } 29 | }) 30 | 31 | // Helper function to get Transfer event logs from the transaction receipt 32 | const getLogs = ( 33 | contract: BaseContract, 34 | receipt: ContractTransactionReceipt|null, 35 | event: string = 'TransferSingle' 36 | ): LogDescription[] => { 37 | if (! receipt) return [] 38 | 39 | const hash = contract.interface.getEvent(event)?.topicHash 40 | return receipt.logs 41 | .filter(log => log.topics[0] === hash) 42 | .map(log => contract.interface.parseLog(log as unknown as ({ topics: string[]; data: string; })) as LogDescription) 43 | } 44 | 45 | describe('Infinity', () => { 46 | let contract: Infinity, 47 | receiveBlockContract: ReceiveBlock, 48 | owner: SignerWithAddress, 49 | addr1: SignerWithAddress, 50 | addr2: SignerWithAddress, 51 | addr3: SignerWithAddress, 52 | addr4: SignerWithAddress, 53 | addr5: SignerWithAddress, 54 | jalil: SignerWithAddress, 55 | vv: SignerWithAddress 56 | 57 | beforeEach(async () => { 58 | ({ contract, owner, addr1, addr2, addr3, addr4, addr5, vv, jalil, receiveBlockContract } = await deployContract()) 59 | }) 60 | 61 | context('Deployment', () => { 62 | it(`Should deploy the contract correctly`, async () => { 63 | expect(await contract.name()).to.equal('Infinity') 64 | expect(await contract.symbol()).to.equal('∞') 65 | }) 66 | 67 | it(`Should set the right price`, async () => { 68 | expect(await contract.PRICE()).to.equal(PRICE) 69 | }) 70 | 71 | it(`Should deploy with genesis live token recipients`, async () => { 72 | expect(await contract.balanceOf(JALIL, 0)).to.equal(1n) 73 | }) 74 | }) 75 | 76 | context('Ether Receive Hook', () => { 77 | it(`mint x tokens of the same tokenId when sending the amount for x`, async () => { 78 | const amounts = [1n, 3n, 9n] 79 | 80 | for (const amount of amounts) { 81 | const tx = await owner.sendTransaction({ to: await contract.getAddress(), value: PRICE*amount }) 82 | const receipt = await tx.wait() 83 | const logData = getLogs(contract, receipt as ContractTransactionReceipt)[0] 84 | 85 | expect(logData.args.from).to.equal(ZeroAddress) 86 | expect(logData.args.to).to.equal(owner.address) 87 | expect(logData.args.value).to.equal(amount) 88 | } 89 | }) 90 | 91 | it(`mint no token when sending too little`, async () => { 92 | await expect(owner.sendTransaction({ to: await contract.getAddress(), value: parseEther('0.004') })) 93 | .to.be.revertedWithCustomError(contract, 'InvalidDeposit()') 94 | }) 95 | 96 | it(`refund remaining after minting/not minting tokens`, async () => { 97 | await expect(owner.sendTransaction({ to: await contract.getAddress(), value: parseEther('0.012') })) 98 | .to.changeEtherBalance(owner, PRICE * -1n) 99 | }) 100 | 101 | it(`fails if remaining cannot be sent`, async () => { 102 | await expect(receiveBlockContract.send(await contract.getAddress(), { value: parseEther('0.012') })) 103 | .to.be.revertedWithCustomError(receiveBlockContract, 'FailedProxySend()') 104 | }) 105 | 106 | it(`Should allow minting by depositing ETH`, async () => { 107 | const tx = await owner.sendTransaction({ to: await contract.getAddress(), value: PRICE }) 108 | const receipt = await tx.wait() 109 | const logData = getLogs(contract, receipt as ContractTransactionReceipt)[0] 110 | 111 | expect(logData.args.from).to.equal(ZeroAddress) 112 | expect(logData.args.to).to.equal(owner.address) 113 | }) 114 | 115 | it(`Should send surplus ETH back when minting by depositing ETH`, async () => { 116 | expect(await owner.sendTransaction({ to: await contract.getAddress(), value: PRICE + parseEther('0.015') })) 117 | .to.changeEtherBalance(owner, PRICE * BigInt(-2)) 118 | }) 119 | }) 120 | 121 | context('Generate', () => { 122 | it(`Shouldn't allow minting for free`, async () => { 123 | await expect(contract.connect(vv).generate(addr1.address, '')) 124 | .to.be.reverted 125 | }) 126 | 127 | it(`only emit message when message is given`, async () => { 128 | await expect(contract.generate(addr1.address, 'The beginning of infinity', { value: PRICE })) 129 | .to.emit(contract, 'Message') 130 | 131 | await expect(contract.generate(addr1.address, '', { value: PRICE })) 132 | .not.to.emit(contract, 'Message') 133 | }) 134 | 135 | it(`fails if recipient is zero address`, async () => { 136 | await expect(contract.generate(ZeroAddress, '', { value: PRICE })) 137 | .to.be.revertedWith('ERC1155: mint to the zero address') 138 | }) 139 | 140 | it(`Should allow minting without a message`, async () => { 141 | await expect(contract.generate(addr1.address, '', { value: PRICE })) 142 | .to.emit(contract, 'TransferSingle') 143 | // TODO: Test parameters manually 144 | }) 145 | 146 | it(`Should allow minting with a message`, async () => { 147 | await expect(contract.generate(addr1.address, 'The beginning of infinity', { value: PRICE })) 148 | .to.emit(contract, 'TransferSingle') 149 | .to.emit(contract, 'Message') 150 | // TODO: Test parameters manually 151 | }) 152 | 153 | it(`Should allow minting many of the same token`, async () => { 154 | let amount = 1 155 | 156 | // Generate an initial one 157 | await contract.connect(vv).generate(addr1.address, '', { value: PRICE }) 158 | 159 | while (amount < 512) { 160 | await expect(contract.generate(addr1.address, '', { value: PRICE * BigInt(amount) })) 161 | .to.emit(contract, 'TransferSingle') 162 | // TODO: Test parameters manually 163 | 164 | amount *= 2 165 | } 166 | }) 167 | }) 168 | 169 | context('GenerateExisting', () => { 170 | it(`fails if source address does not have any token with tokenId`, async () => { 171 | await expect(contract.generateExisting(JALIL, addr1.address, 123, '', { value: PRICE })) 172 | .to.revertedWithCustomError(contract, 'InvalidToken()') 173 | }) 174 | 175 | it(`works for non existing tokenIds if VV`, async () => { 176 | await expect(contract.connect(vv).generateExisting(ZeroAddress, addr1.address, 123, '', { value: PRICE })) 177 | .to.emit(contract, 'TransferSingle') 178 | }) 179 | 180 | it(`Should mark tokens as minted when VV mints them`, async () => { 181 | await expect(contract.connect(vv).generateExisting(ZeroAddress, vv.address, 0, '', { value: PRICE })) 182 | .to.emit(contract, 'TransferSingle') 183 | .withArgs(vv.address, ZeroAddress, vv.address, 0, 1) 184 | 185 | await expect(contract.connect(vv).generateExisting(ZeroAddress, owner.address, 2, '', { value: PRICE })) 186 | .to.emit(contract, 'TransferSingle') 187 | .withArgs(vv.address, ZeroAddress, owner.address, 2, 1) 188 | 189 | await expect(contract.connect(vv).generateManyExisting( 190 | [ZeroAddress, ZeroAddress, ZeroAddress, ZeroAddress], 191 | [owner.address, addr1.address, addr2.address, addr3.address], 192 | [1, 2, 3, 4], 193 | [1, 1, 1, 1], 194 | { value: PRICE * BigInt(4) } 195 | )) 196 | .to.emit(contract, 'TransferSingle') 197 | .withArgs(vv.address, ZeroAddress, owner.address, 1, 1) 198 | .to.emit(contract, 'TransferSingle') 199 | .withArgs(vv.address, ZeroAddress, addr1.address, 2, 1) 200 | .to.emit(contract, 'TransferSingle') 201 | .withArgs(vv.address, ZeroAddress, addr2.address, 3, 1) 202 | .to.emit(contract, 'TransferSingle') 203 | .withArgs(vv.address, ZeroAddress, addr3.address, 4, 1) 204 | 205 | // It should then allow others to mint these as well 206 | await expect(contract.connect(addr3).generateExisting(owner.address, addr3.address, 1, '', { value: PRICE })) 207 | .to.emit(contract, 'TransferSingle') 208 | .withArgs(addr3.address, ZeroAddress, addr3.address, 1, 1) 209 | 210 | await expect(contract.connect(addr3).generateExisting(owner.address, addr3.address, 2, '', { value: PRICE })) 211 | .to.emit(contract, 'TransferSingle') 212 | .withArgs(addr3.address, ZeroAddress, addr3.address, 2, 1) 213 | }) 214 | }) 215 | 216 | context('Regenerate', () => { 217 | it(`fails if sender does not have amount of tokenId`, async () => { 218 | await expect(contract.connect(jalil).regenerate(0, 2)) 219 | .to.revertedWith('ERC1155: burn amount exceeds balance') 220 | 221 | await expect(contract.connect(jalil).regenerate(0, 1)) 222 | .not.to.be.reverted 223 | }) 224 | 225 | it(`fails if tokenId does not exist (should be the same reason as above)`, async () => { 226 | await expect(contract.connect(jalil).regenerate(123, 1)) 227 | .to.revertedWith('ERC1155: burn amount exceeds balance') 228 | }) 229 | 230 | it(`mint the same amount of tokens burned of a random id`, async () => { 231 | const createTx = await jalil.sendTransaction({ to: await contract.getAddress(), value: PRICE*5n }) 232 | const createReceipt = await createTx.wait() 233 | const createLog = getLogs(contract, createReceipt as ContractTransactionReceipt)[0] 234 | const tokenId = createLog.args.id 235 | 236 | const regenerateTx = await contract.connect(jalil).regenerate(tokenId, 5n) 237 | const regenerateReceipt = await regenerateTx.wait() 238 | const regenerateLog = getLogs(contract, regenerateReceipt as ContractTransactionReceipt) 239 | 240 | const newTokenId = regenerateLog[1].args.id 241 | expect(regenerateLog[0].args).to.deep.equal([ 242 | jalil.address, 243 | jalil.address, 244 | ZeroAddress, 245 | tokenId, 246 | 5n 247 | ]) 248 | expect(regenerateLog[1].args).to.deep.equal([ 249 | jalil.address, 250 | ZeroAddress, 251 | jalil.address, 252 | newTokenId, 253 | 5n 254 | ]) 255 | }) 256 | 257 | it(`Should not allow regenerating tokens we don't own`, async () => { 258 | await contract.connect(vv).generateExisting(ZeroAddress, vv.address, 0, "", { value: PRICE }) 259 | await contract.connect(vv).generateExisting( 260 | ZeroAddress, 261 | addr5.address, 262 | 7, 263 | "", 264 | { value: PRICE * BigInt(2) } 265 | ) 266 | 267 | await expect(contract.connect(addr4).regenerate(7, 1)) 268 | .to.be.revertedWith(`ERC1155: burn amount exceeds balance`) 269 | await expect(contract.connect(addr5).regenerate(7, 5)) 270 | .to.be.revertedWith(`ERC1155: burn amount exceeds balance`) 271 | 272 | expect(await contract.balanceOf(addr5.address, 7)).to.equal(2) 273 | expect(await contract.balanceOf(addr5.address, 0)).to.equal(0) 274 | }) 275 | 276 | it(`Should allow regenerating tokens`, async () => { 277 | await contract.connect(vv).generateExisting(ZeroAddress, vv.address, 0, "", { value: PRICE }) 278 | await contract.connect(vv).generateExisting( 279 | ZeroAddress, 280 | addr5.address, 281 | 8, 282 | "", 283 | { value: PRICE * BigInt(2) } 284 | ) 285 | 286 | await expect(await contract.connect(addr5).regenerate(8, 2)) 287 | .to.emit(contract, 'TransferSingle') 288 | .to.emit(contract, 'TransferSingle') 289 | // TODO: Check new token ID balances 290 | 291 | expect(await contract.balanceOf(addr5.address, 8)).to.equal(0) 292 | }) 293 | }) 294 | 295 | context('Degenerate', () => { 296 | it(`fails if sender does not have amount of tokenId`, async () => { 297 | await expect(contract.connect(jalil).degenerate(0, 2)) 298 | .to.revertedWith('ERC1155: burn amount exceeds balance') 299 | 300 | await expect(contract.connect(jalil).degenerate(0, 1)) 301 | .not.to.be.reverted 302 | }) 303 | 304 | it(`fails if tokenId does not exist (should be the same reason as above)`, async () => { 305 | await expect(contract.connect(jalil).degenerate(123, 1)) 306 | .to.revertedWith('ERC1155: burn amount exceeds balance') 307 | }) 308 | 309 | it(`refunds correct amount if sender did have amount of tokenId`, async () => { 310 | await expect(contract.connect(jalil).degenerate(0, 1)) 311 | .to.changeEtherBalance(jalil, PRICE) 312 | }) 313 | 314 | it(`Should allow degenerating`, async () => { 315 | const TOKEN = 0 316 | await contract.connect(vv).generateExisting(ZeroAddress, addr3.address, TOKEN, '', { value: PRICE * BigInt(10) }) 317 | const balance = await contract.balanceOf(addr3.address, TOKEN) 318 | 319 | await expect(contract.connect(addr3).degenerate(TOKEN, 1)) 320 | .to.emit(contract, 'TransferSingle') 321 | .withArgs(addr3.address, addr3.address, ZeroAddress, TOKEN, 1) 322 | 323 | expect(await contract.balanceOf(addr3.address, TOKEN)).to.equal(balance - 1n) 324 | }) 325 | 326 | it(`Should allow withdrawing funds with specified token amount`, async () => { 327 | const TOKEN = 0 328 | await contract.connect(vv).generateExisting(ZeroAddress, addr2.address, TOKEN, '', { value: PRICE * BigInt(10) }) 329 | await expect(contract.connect(addr2).degenerate(TOKEN, 50)).to.be.revertedWith(`ERC1155: burn amount exceeds balance`) 330 | 331 | expect(await contract.connect(addr2).degenerate(TOKEN, 5)) 332 | .to.changeEtherBalance(addr2, PRICE * BigInt(5)) 333 | }) 334 | }) 335 | 336 | context('GenerateMany', () => { 337 | it(`fails if recipients and amounts have different length`, async () => { 338 | await expect(contract.generateMany( 339 | [addr1.address, addr2.address], 340 | [1n, 2n, 3n], 341 | { value: PRICE*3n } 342 | )).to.be.revertedWithCustomError(contract, 'InvalidInput') 343 | 344 | await expect(contract.generateMany( 345 | [addr1.address, addr2.address, addr3.address], 346 | [1n, 2n], 347 | { value: PRICE*3n } 348 | )).to.be.revertedWithCustomError(contract, 'InvalidInput') 349 | 350 | await expect(contract.generateMany( 351 | [addr1.address, addr2.address], 352 | [1n, 2n, 3n], 353 | { value: PRICE*6n } 354 | )).to.be.revertedWithCustomError(contract, 'InvalidInput') 355 | }) 356 | 357 | it(`fails if not sending the exact amount for nr tokens minted`, async () => { 358 | await expect(contract.generateMany( 359 | [addr1.address, addr2.address], 360 | [1n, 2n], 361 | { value: PRICE*4n } 362 | )).to.be.revertedWithCustomError(contract, 'InvalidDeposit') 363 | 364 | await expect(contract.generateMany( 365 | [addr1.address, addr2.address], 366 | [1n, 2n], 367 | { value: PRICE*3n } 368 | )).not.to.be.reverted 369 | }) 370 | 371 | it(`mint amount of different random tokenIds to given recipients`, async () => { 372 | await expect(contract.generateMany( 373 | [addr1.address, addr2.address], 374 | [1n, 2n], 375 | { value: PRICE*3n } 376 | )).to.emit(contract, 'TransferSingle') 377 | .to.emit(contract, 'TransferSingle') 378 | }) 379 | 380 | it(`fails if any recipient is zero address`, async () => { 381 | await expect(contract.generateMany( 382 | [addr1.address, ZeroAddress], 383 | [1n, 2n], 384 | { value: PRICE*3n } 385 | )).to.be.revertedWith('ERC1155: mint to the zero address') 386 | }) 387 | 388 | it(`fails if any amount is zero`, async () => { 389 | await expect(contract.generateMany( 390 | [addr1.address, addr2.address], 391 | [1n, 0n], 392 | { value: PRICE } 393 | )).to.be.revertedWithCustomError(contract, 'InvalidInput') 394 | }) 395 | 396 | it(`Should allow minting many different tokens`, async () => { 397 | const tx = await contract.connect(addr1).generateMany( 398 | [addr1.address, addr1.address], 399 | [9, 19], 400 | { value: PRICE * BigInt(9 + 19) } 401 | ) 402 | const logs = getLogs(contract, await tx.wait()) 403 | const log1Data = logs[0] as LogDescription 404 | const log2Data = logs[1] as LogDescription 405 | expect(log1Data.args.from).to.equal(ZeroAddress) 406 | expect(log2Data.args.from).to.equal(ZeroAddress) 407 | expect(log1Data.args.to).to.equal(addr1.address) 408 | expect(log2Data.args.to).to.equal(addr1.address) 409 | expect(log1Data.args.id).not.to.equal(log2Data.args.id) 410 | expect(log1Data.args.value).to.equal(9) 411 | expect(log2Data.args.value).to.equal(19) 412 | }) 413 | 414 | it(`Shouldn't let users define more recipients than amounts`, async () => { 415 | await expect(contract.generateMany( 416 | [addr1.address, addr2.address, addr3.address], 417 | [10, 1], 418 | { value: PRICE * BigInt(11) } 419 | )) 420 | .to.be.revertedWithCustomError(contract, `InvalidInput()`) 421 | }) 422 | }) 423 | 424 | context('GenerateManyExisting', () => { 425 | it(`fails if arguments have different lengths`, async () => { 426 | await expect(contract.generateManyExisting( 427 | [jalil.address, jalil.address], 428 | [addr1.address, addr2.address], 429 | [0n, 0n], 430 | [1n, 2n, 3n], 431 | { value: PRICE*3n } 432 | )).to.be.revertedWithCustomError(contract, 'InvalidInput') 433 | 434 | await expect(contract.generateManyExisting( 435 | [jalil.address, jalil.address], 436 | [addr1.address, addr2.address, addr3.address], 437 | [0n, 0n], 438 | [1n, 2n, 3n], 439 | { value: PRICE*3n } 440 | )).to.be.revertedWithCustomError(contract, 'InvalidInput') 441 | }) 442 | 443 | it(`fails if not sending the exact amount for nr tokens minted`, async () => { 444 | await expect(contract.generateManyExisting( 445 | [jalil.address, jalil.address], 446 | [addr1.address, addr2.address], 447 | [0n, 0n], 448 | [1n, 2n], 449 | { value: PRICE*4n } 450 | )).to.be.revertedWithCustomError(contract, 'InvalidDeposit') 451 | 452 | await expect(contract.generateManyExisting( 453 | [jalil.address, jalil.address], 454 | [addr1.address, addr2.address], 455 | [0n, 0n], 456 | [1n, 2n], 457 | { value: PRICE*3n } 458 | )).not.to.be.reverted 459 | }) 460 | 461 | it(`fails if any source address does not have any token with tokenId`, async () => { 462 | await expect(contract.generateManyExisting( 463 | [jalil.address, addr3.address], 464 | [addr1.address, addr2.address], 465 | [0n, 0n], 466 | [1n, 2n], 467 | { value: PRICE*3n } 468 | )).to.be.revertedWithCustomError(contract, 'InvalidToken') 469 | }) 470 | 471 | it(`works for non existing tokenIds if VV`, async () => { 472 | await expect(contract.connect(vv).generateManyExisting( 473 | [ZeroAddress, ZeroAddress], 474 | [addr1.address, addr2.address], 475 | [95n, 99n], 476 | [1n, 2n], 477 | { value: PRICE*3n } 478 | )).not.to.be.reverted 479 | }) 480 | 481 | it(`fails if any recipient is zero address`, async () => { 482 | await expect(contract.connect(vv).generateManyExisting( 483 | [ZeroAddress, ZeroAddress], 484 | [addr1.address, ZeroAddress], 485 | [95n, 99n], 486 | [1n, 2n], 487 | { value: PRICE*3n } 488 | )).to.be.revertedWith('ERC1155: mint to the zero address') 489 | }) 490 | 491 | it(`fails if any amount is zero`, async () => { 492 | await expect(contract.connect(vv).generateManyExisting( 493 | [ZeroAddress, ZeroAddress], 494 | [addr1.address, ZeroAddress], 495 | [95n, 99n], 496 | [0n, 2n], 497 | { value: PRICE*2n } 498 | )).to.be.revertedWithCustomError(contract, 'InvalidInput') 499 | }) 500 | 501 | it(`mint amount of existing tokenIds to given recipients`, async () => { 502 | await expect(contract.generateManyExisting( 503 | [jalil.address, jalil.address], 504 | [addr1.address, addr2.address], 505 | [0n, 0n], 506 | [1n, 2n], 507 | { value: PRICE*3n } 508 | )).to.emit(contract, 'TransferSingle') 509 | .to.emit(contract, 'TransferSingle') 510 | }) 511 | 512 | it(`Shouldn't let users overpay when generating many`, async () => { 513 | await expect(contract.generateMany( 514 | [addr1.address, addr2.address], 515 | [10, 1, 1], 516 | { value: PRICE * BigInt(12) } 517 | )) 518 | .to.be.revertedWithCustomError(contract, `InvalidInput()`) 519 | 520 | await contract.connect(vv).generateExisting(ZeroAddress, vv.address, 9, '', { value: PRICE }) 521 | await expect(contract.connect(addr1).generateManyExisting( 522 | [ZeroAddress, ZeroAddress], 523 | [addr1.address, addr2.address, addr3.address], 524 | [0, 9], 525 | [10, 2], 526 | { value: PRICE * BigInt(12) } 527 | )).to.be.revertedWithCustomError(contract, `InvalidInput()`) 528 | await expect(contract.connect(addr1).generateManyExisting( 529 | [ZeroAddress, ZeroAddress, ZeroAddress], 530 | [addr1.address, addr2.address, addr3.address], 531 | [0, 9, 0], 532 | [10, 2], 533 | { value: PRICE * BigInt(12) } 534 | )).to.be.revertedWithCustomError(contract, `InvalidInput()`) 535 | }) 536 | }) 537 | 538 | context('RegenerateMany', () => { 539 | let tokenIds: bigint[] = [] 540 | 541 | beforeEach(async () => { 542 | const createTx = await contract.generateMany( 543 | [jalil.address, jalil.address], 544 | [5n, 9n], 545 | { value: PRICE*14n } 546 | ) 547 | const createReceipt = await createTx.wait() 548 | const createLog = getLogs(contract, createReceipt as ContractTransactionReceipt) 549 | 550 | tokenIds = createLog.map(log => log.args.id) 551 | }) 552 | 553 | it(`fails if arguments have different lengths`, async () => { 554 | await expect(contract.connect(jalil).regenerateMany( 555 | tokenIds, 556 | [0n, 9n, 5n], 557 | )).to.be.revertedWithCustomError(contract, 'InvalidInput') 558 | 559 | await expect(contract.connect(jalil).regenerateMany( 560 | [0n, ...tokenIds], 561 | [9n, 5n], 562 | )).to.be.revertedWithCustomError(contract, 'InvalidInput') 563 | }) 564 | 565 | it(`fails if sender does not have amount of any tokenId`, async () => { 566 | await expect(contract.connect(jalil).regenerateMany( 567 | tokenIds, 568 | [9n, 5n], 569 | )).to.be.revertedWith('ERC1155: burn amount exceeds balance') 570 | 571 | await expect(contract.connect(jalil).regenerateMany( 572 | tokenIds, 573 | [5n, 9n], 574 | )).not.to.be.reverted 575 | }) 576 | 577 | it(`mint and burn the same amount of tokens`, async () => { 578 | const tx = await contract.connect(jalil).regenerateMany( 579 | tokenIds, 580 | [5n, 9n], 581 | ) 582 | const receipt = await tx.wait() 583 | const log = getLogs(contract, receipt as ContractTransactionReceipt) 584 | 585 | expect(log[0].args).to.deep.equal([ 586 | jalil.address, 587 | jalil.address, 588 | ZeroAddress, 589 | tokenIds[0], 590 | 5n 591 | ]) 592 | expect(log[1].args).to.deep.equal([ 593 | jalil.address, 594 | ZeroAddress, 595 | jalil.address, 596 | log[1].args.id, 597 | 5n 598 | ]) 599 | expect(log[2].args).to.deep.equal([ 600 | jalil.address, 601 | jalil.address, 602 | ZeroAddress, 603 | tokenIds[1], 604 | 9n 605 | ]) 606 | expect(log[3].args).to.deep.equal([ 607 | jalil.address, 608 | ZeroAddress, 609 | jalil.address, 610 | log[3].args.id, 611 | 9n 612 | ]) 613 | }) 614 | 615 | it(`Should allow regenerating multiple tokens at once`, async () => { 616 | await contract.connect(vv).generateExisting(ZeroAddress, vv.address, 19, "", { value: PRICE }) 617 | 618 | await contract.connect(vv).generateManyExisting( 619 | [ZeroAddress, ZeroAddress], 620 | [addr5.address, addr5.address], 621 | [9, 10], 622 | [2, 2], 623 | { value: PRICE * BigInt(4) } 624 | ) 625 | 626 | await expect(contract.connect(addr5).regenerateMany( 627 | [9, 10], 628 | [2, 3], 629 | )).to.be.revertedWith(`ERC1155: burn amount exceeds balance`) 630 | 631 | await expect(await contract.connect(addr5).regenerateMany( 632 | [9, 10], 633 | [2, 2], 634 | )) 635 | .to.emit(contract, 'TransferSingle') 636 | .withArgs(addr5.address, addr5.address, ZeroAddress, 9, 2) 637 | .to.emit(contract, 'TransferSingle') 638 | .withArgs(addr5.address, addr5.address, ZeroAddress, 10, 2) 639 | .to.emit(contract, 'TransferSingle') 640 | 641 | expect(await contract.balanceOf(addr5.address, 9)).to.equal(0) 642 | expect(await contract.balanceOf(addr5.address, 10)).to.equal(0) 643 | // TODO: Check new token balance 644 | }) 645 | }) 646 | 647 | context('DegenerateMany', () => { 648 | let tokenIds: bigint[] = [] 649 | 650 | beforeEach(async () => { 651 | const createTx = await contract.generateMany( 652 | [jalil.address, jalil.address], 653 | [5n, 9n], 654 | { value: PRICE*14n } 655 | ) 656 | const createReceipt = await createTx.wait() 657 | const createLog = getLogs(contract, createReceipt as ContractTransactionReceipt) 658 | 659 | tokenIds = createLog.map(log => log.args.id) 660 | }) 661 | 662 | it(`fails if arguments have different lengths`, async () => { 663 | await expect(contract.connect(jalil).degenerateMany( 664 | tokenIds, 665 | [0n, 9n, 5n], 666 | )).to.be.revertedWithCustomError(contract, 'InvalidInput') 667 | 668 | await expect(contract.connect(jalil).degenerateMany( 669 | [0n, ...tokenIds], 670 | [9n, 5n], 671 | )).to.be.revertedWithCustomError(contract, 'InvalidInput') 672 | }) 673 | 674 | it(`refunds correct value if all tokens could be burnt`, async () => { 675 | await expect(contract.connect(jalil).degenerateMany( 676 | tokenIds, 677 | [5n, 9n], 678 | )).to.changeEtherBalance(jalil, PRICE * 14n) 679 | }) 680 | 681 | it(`Shouldn't allow minting batches with an invalid deposit`, async () => { 682 | await expect(contract.generateMany( 683 | [addr1.address, addr2.address], 684 | [10, 1], 685 | { value: PRICE * BigInt(1) } 686 | )) 687 | .to.be.revertedWithCustomError(contract, `InvalidDeposit()`) 688 | }) 689 | 690 | it(`Should allow withdrawing funds for many tokens`, async () => { 691 | const TOKEN = 0 692 | await contract.connect(vv).generateManyExisting( 693 | [ZeroAddress, ZeroAddress, ZeroAddress, ZeroAddress], 694 | [addr4.address, addr4.address, addr5.address, addr5.address], 695 | [TOKEN, TOKEN + 1, TOKEN, TOKEN + 1], 696 | [10, 10, 10, 10], 697 | { value: PRICE * BigInt(40) } 698 | ) 699 | await expect(contract.connect(addr4).degenerateMany([TOKEN, TOKEN+1], [5, 5])) 700 | .to.emit(contract, 'TransferBatch') 701 | .withArgs(addr4.address, addr4.address, ZeroAddress, [TOKEN, TOKEN+1], [5, 5]) 702 | 703 | expect(await contract.connect(addr5).degenerateMany([TOKEN, TOKEN+1], [5, 5])) 704 | .to.changeEtherBalance(addr5, PRICE * BigInt(10)) 705 | }) 706 | }) 707 | 708 | context('Transfers', () => { 709 | it(`Should allow transferring assets`, async () => { 710 | const tx = await contract.connect(addr2).generate(addr2.address, '', { value: PRICE * BigInt(10) }) 711 | const logData = getLogs(contract, await tx.wait())[0] 712 | const ID = logData.args.id.toString() 713 | expect(logData.args.from).to.equal(ZeroAddress) 714 | expect(logData.args.to).to.equal(addr2.address) 715 | expect(logData.args.value).to.equal(10) 716 | 717 | expect(await contract.balanceOf(addr2.address, ID)).to.equal(10) 718 | 719 | await expect(contract.connect(addr2).safeTransferFrom(addr2.address, addr3.address, ID, 1, toBeArray(0))) 720 | .to.emit(contract, 'TransferSingle') 721 | .withArgs(addr2.address, addr2.address, addr3.address, ID, 1) 722 | 723 | expect(await contract.balanceOf(addr2.address, ID)).to.equal(9) 724 | expect(await contract.balanceOf(addr3.address, ID)).to.equal(1) 725 | }) 726 | 727 | it(`Should allow minting and transferring batches`, async () => { 728 | const tx = await contract.generateMany( 729 | [addr4.address, addr4.address, addr5.address], 730 | [10, 10, 1], 731 | { value: PRICE * BigInt(21) } 732 | ) 733 | const logData = getLogs(contract, await tx.wait()) 734 | expect(logData.length).to.equal(3) 735 | expect(logData[0].args.from).to.equal(ZeroAddress) 736 | expect(logData[0].args.to).to.equal(addr4.address) 737 | expect(logData[0].args.value).to.equal(10) 738 | expect(logData[2].args.to).to.equal(addr5.address) 739 | const ID1 = logData[0].args.id 740 | const ID2 = logData[1].args.id 741 | const ID3 = logData[2].args.id 742 | 743 | await expect(contract.connect(addr4).safeBatchTransferFrom( 744 | addr4.address, 745 | addr5.address, 746 | [ID1, ID2], 747 | [5, 4], 748 | toBeArray(0) 749 | )) 750 | .to.emit(contract, 'TransferBatch') 751 | .withArgs(addr4.address, addr4.address, addr5.address, [ID1, ID2], [5, 4]) 752 | 753 | expect((await contract 754 | .connect(addr4) 755 | .balanceOfBatch( 756 | [addr4.address, addr4.address, addr4.address, addr5.address, addr5.address, addr5.address], 757 | [ID1, ID2, ID3, ID1, ID2, ID3] 758 | )).map((n: BigInt) => Number(n)) 759 | ).to.deep.equal([5, 6, 0, 5, 4, 1]) 760 | }) 761 | }) 762 | 763 | context.skip(`Rendering`, () => { 764 | it(`Renders token SVGs`, async () => { 765 | let id = 0; 766 | 767 | while (id < 50) { 768 | const svg = await contract.svg(id) 769 | 770 | fs.writeFileSync(`test/dist/${id}.svg`, svg) 771 | 772 | console.log(`Rendered ${id}`) 773 | 774 | id++ 775 | } 776 | }) 777 | 778 | it(`Renders Black Check SVGs`, async () => { 779 | let id = 0; 780 | 781 | while (id < 15_000) { 782 | const svg = await contract.svg(id) 783 | 784 | fs.writeFileSync(`test/dist/${id}.svg`, svg) 785 | 786 | console.log(`Rendered ${id}`) 787 | 788 | id += 4096 789 | } 790 | }) 791 | 792 | it(`Renders token metadata`, async () => { 793 | let id = 0; 794 | 795 | while (id < 50) { 796 | const metadata = decodeBase64URI(await contract.uri(id)) 797 | 798 | fs.writeFileSync(`test/dist/${id}.json`, JSON.stringify(metadata, null, 4)) 799 | 800 | console.log(`Saved metadata for ${id}`) 801 | 802 | id++ 803 | } 804 | }) 805 | 806 | it(`Renders to PNGs`, async () => { 807 | await render('test/dist') 808 | }) 809 | }) 810 | 811 | }) 812 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "declaration": true, 9 | "resolveJsonModule": true 10 | }, 11 | "include": ["./scripts", "./test", "./deploy", "./typechain-types"], 12 | "files": ["./hardhat.config.ts"] 13 | } 14 | --------------------------------------------------------------------------------