├── .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 | ''
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 |
--------------------------------------------------------------------------------