Skip to content

Commit c69da8b

Browse files
committed
Add 1155 randomly assigned extension. Fixes #17
1 parent 32b3c17 commit c69da8b

File tree

5 files changed

+235
-1
lines changed

5 files changed

+235
-1
lines changed

.changeset/salty-colts-arrive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@1001-digital/erc721-extensions": patch
3+
---
4+
5+
Add 1155 randomly assigned extension (fixes #17)

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ERC721 Contract Extensions
22

3-
A set of composable extensions for the [OpenZeppelin](https://openzeppelin.com/) ERC721 base contracts.
3+
A set of composable extensions for the [OpenZeppelin](https://openzeppelin.com/) ERC721 and ERC1155 base contracts.
44

55
> **v0.1.0 Breaking Change**: This version targets OpenZeppelin 5.x and Solidity ^0.8.20. It is not backward compatible with OZ 4.x consumers. All `require` strings have been replaced with custom errors, `Counters` has been removed, and `_mint`/`_transfer` overrides have been replaced with the OZ 5.x `_update` hook.
66
@@ -351,6 +351,30 @@ contract ExpandableCollection is WithAdditionalMints {
351351

352352
After the initial collection is live, the owner can call `addToken(cid)`, `addTokens(cid, count)`, `mintAdditionalToken(cid, to)`, or `mintAdditionalTokens(cid, count, to)` to increase supply and point metadata to the updated IPFS directory.
353353

354+
### `RandomlyAssigned1155.sol`
355+
356+
Randomly assign ERC1155 token IDs from a set of token types, each with its own supply. On each mint, a token type is selected at random weighted by its remaining supply.
357+
358+
```solidity
359+
contract RandomPack is ERC1155, RandomlyAssigned1155 {
360+
constructor()
361+
ERC1155("")
362+
RandomlyAssigned1155(
363+
new uint256[](3) /* [1, 2, 3] */,
364+
new uint256[](3) /* [10000, 10000, 10000] */
365+
)
366+
{}
367+
368+
function mint(uint256 amount) external ensureAvailabilityFor(amount) {
369+
for (uint256 i = 0; i < amount; i++) {
370+
_mint(msg.sender, nextToken(), 1, "");
371+
}
372+
}
373+
}
374+
```
375+
376+
> Uses the same on-chain randomness approach as `RandomlyAssigned`. The weighted selection loop is negligible for typical ERC1155 collections (dozens of token types).
377+
354378
## Local Development
355379

356380
This project uses [Hardhat](https://hardhat.org/) and [pnpm](https://pnpm.io/).

contracts/RandomlyAssigned1155.sol

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
/// @author 1001.digital
5+
/// @title Randomly assign ERC1155 token IDs from a set of token types with individual supplies.
6+
abstract contract RandomlyAssigned1155 {
7+
/// @dev Thrown when no tokens remain to be minted.
8+
error NoTokensAvailable();
9+
/// @dev Thrown when the requested mint amount exceeds available supply.
10+
error RequestedTokensNotAvailable();
11+
/// @dev Thrown when token ID and supply array lengths don't match.
12+
error TokenIdSupplyMismatch();
13+
14+
// The available token IDs
15+
uint256[] private _tokenIds;
16+
17+
// Remaining supply per token ID
18+
mapping(uint256 => uint256) private _remaining;
19+
20+
// Total remaining across all token IDs
21+
uint256 private _totalRemaining;
22+
23+
/// Instanciate the contract
24+
/// @param tokenIds_ the token IDs available for minting
25+
/// @param supplies_ the max supply for each token ID
26+
constructor(uint256[] memory tokenIds_, uint256[] memory supplies_) {
27+
if (tokenIds_.length != supplies_.length) revert TokenIdSupplyMismatch();
28+
29+
for (uint256 i = 0; i < tokenIds_.length; i++) {
30+
_tokenIds.push(tokenIds_[i]);
31+
_remaining[tokenIds_[i]] = supplies_[i];
32+
_totalRemaining += supplies_[i];
33+
}
34+
}
35+
36+
/// @dev Get the total remaining token count across all token types
37+
/// @return the available token count
38+
function availableTokenCount() public view returns (uint256) {
39+
return _totalRemaining;
40+
}
41+
42+
/// @dev Get the remaining supply for a specific token ID
43+
/// @return the remaining supply
44+
function availableSupplyOf(uint256 tokenId) public view returns (uint256) {
45+
return _remaining[tokenId];
46+
}
47+
48+
/// Get the next token ID
49+
/// @dev Randomly selects a token ID weighted by remaining supply.
50+
/// @return the selected token ID
51+
function nextToken() internal virtual returns (uint256) {
52+
if (_totalRemaining == 0) revert NoTokensAvailable();
53+
54+
uint256 random = uint256(keccak256(
55+
abi.encodePacked(
56+
msg.sender,
57+
block.coinbase,
58+
block.prevrandao,
59+
block.gaslimit,
60+
block.timestamp
61+
)
62+
)) % _totalRemaining;
63+
64+
uint256 cumulative = 0;
65+
uint256 selectedId;
66+
for (uint256 i = 0; i < _tokenIds.length; i++) {
67+
cumulative += _remaining[_tokenIds[i]];
68+
if (random < cumulative) {
69+
selectedId = _tokenIds[i];
70+
break;
71+
}
72+
}
73+
74+
_remaining[selectedId]--;
75+
_totalRemaining--;
76+
77+
return selectedId;
78+
}
79+
80+
/// @dev Check whether another token is still available
81+
modifier ensureAvailability() {
82+
if (_totalRemaining == 0) revert NoTokensAvailable();
83+
_;
84+
}
85+
86+
/// @param amount Check whether number of tokens are still available
87+
/// @dev Check whether tokens are still available
88+
modifier ensureAvailabilityFor(uint256 amount) {
89+
if (_totalRemaining < amount) revert RequestedTokensNotAvailable();
90+
_;
91+
}
92+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
5+
6+
import "./../RandomlyAssigned1155.sol";
7+
8+
contract RandomlyAssigned1155Example is ERC1155, RandomlyAssigned1155 {
9+
constructor(
10+
uint256[] memory tokenIds,
11+
uint256[] memory supplies
12+
)
13+
ERC1155("")
14+
RandomlyAssigned1155(tokenIds, supplies)
15+
{}
16+
17+
function mint(uint256 amount) external
18+
ensureAvailabilityFor(amount)
19+
{
20+
for (uint256 i = 0; i < amount; i++) {
21+
_mint(msg.sender, nextToken(), 1, "");
22+
}
23+
}
24+
}

test/RandomlyAssigned1155.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import assert from "node:assert/strict";
2+
import { describe, it } from "node:test";
3+
4+
import { network } from "hardhat";
5+
import { parseEventLogs } from "viem";
6+
7+
describe("RandomlyAssigned1155", async function () {
8+
const { viem } = await network.connect();
9+
const publicClient = await viem.getPublicClient();
10+
const [, buyerWallet] = await viem.getWalletClients();
11+
12+
const tokenIds = [1n, 2n, 3n];
13+
const supplies = [5n, 5n, 5n];
14+
15+
it("Deployment should set the correct available supply", async function () {
16+
const contract = await viem.deployContract("RandomlyAssigned1155Example", [tokenIds, supplies]);
17+
18+
assert.equal(await contract.read.availableTokenCount(), 15n);
19+
});
20+
21+
it("Reports available supply per token ID", async function () {
22+
const contract = await viem.deployContract("RandomlyAssigned1155Example", [tokenIds, supplies]);
23+
24+
assert.equal(await contract.read.availableSupplyOf([1n]), 5n);
25+
assert.equal(await contract.read.availableSupplyOf([2n]), 5n);
26+
assert.equal(await contract.read.availableSupplyOf([3n]), 5n);
27+
});
28+
29+
it("Mints all tokens across all types", async function () {
30+
const contract = await viem.deployContract("RandomlyAssigned1155Example", [tokenIds, supplies]);
31+
32+
await contract.write.mint([15n], { account: buyerWallet.account });
33+
34+
for (const id of tokenIds) {
35+
const balance = await contract.read.balanceOf([buyerWallet.account.address, id]);
36+
assert.equal(balance, 5n);
37+
}
38+
39+
assert.equal(await contract.read.availableTokenCount(), 0n);
40+
});
41+
42+
it("Distributes tokens across multiple types", async function () {
43+
const contract = await viem.deployContract("RandomlyAssigned1155Example", [tokenIds, supplies]);
44+
const mintedIds: number[] = [];
45+
46+
for (let i = 0; i < 15; i++) {
47+
const hash = await contract.write.mint([1n], { account: buyerWallet.account });
48+
const receipt = await publicClient.getTransactionReceipt({ hash });
49+
const logs = parseEventLogs({
50+
abi: contract.abi,
51+
logs: receipt.logs,
52+
eventName: "TransferSingle",
53+
});
54+
mintedIds.push(Number(logs[0].args.id));
55+
}
56+
57+
const uniqueTypes = new Set(mintedIds);
58+
assert.equal(uniqueTypes.size, 3, "Expected all three token types to be minted");
59+
});
60+
61+
it("Decreases available supply per token type on mint", async function () {
62+
const contract = await viem.deployContract("RandomlyAssigned1155Example", [tokenIds, supplies]);
63+
64+
await contract.write.mint([1n], { account: buyerWallet.account });
65+
66+
const total = await contract.read.availableTokenCount();
67+
assert.equal(total, 14n);
68+
});
69+
70+
it("Fails when trying to mint more than available", async function () {
71+
const contract = await viem.deployContract("RandomlyAssigned1155Example", [tokenIds, supplies]);
72+
73+
await assert.rejects(
74+
contract.write.mint([16n], { account: buyerWallet.account }),
75+
/RequestedTokensNotAvailable/,
76+
);
77+
});
78+
79+
it("Fails when all tokens are minted", async function () {
80+
const contract = await viem.deployContract("RandomlyAssigned1155Example", [tokenIds, supplies]);
81+
82+
await contract.write.mint([15n], { account: buyerWallet.account });
83+
84+
await assert.rejects(
85+
contract.write.mint([1n], { account: buyerWallet.account }),
86+
/RequestedTokensNotAvailable/,
87+
);
88+
});
89+
});

0 commit comments

Comments
 (0)