From 063213e8caa48a7713d006ae135ac5ef2d64120c Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Wed, 1 May 2024 01:26:42 +0200 Subject: [PATCH] Add maxSupply to ConfigurableGuildRewardNFT --- contracts/ConfigurableGuildRewardNFT.sol | 12 +++ .../IConfigurableGuildRewardNFT.sol | 3 +- .../interfaces/IGuildRewardNFTFactory.sol | 2 + contracts/interfaces/IMaxSupply.sol | 24 ++++++ docs/contracts/ConfigurableGuildRewardNFT.md | 31 ++++++++ .../interfaces/IGuildRewardNFTFactory.md | 1 + docs/contracts/interfaces/IMaxSupply.md | 77 +++++++++++++++++++ scripts/deploy-configurable-nft-zksync.ts | 1 + scripts/deploy-configurable-nft.ts | 1 + test/ConfigurableGuildRewardNFT.spec.ts | 51 ++++++++++++ test/GuildRewardNFTFactory.spec.ts | 3 + 11 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 contracts/interfaces/IMaxSupply.sol create mode 100644 docs/contracts/interfaces/IMaxSupply.md diff --git a/contracts/ConfigurableGuildRewardNFT.sol b/contracts/ConfigurableGuildRewardNFT.sol index 2a30a3e..37b8155 100644 --- a/contracts/ConfigurableGuildRewardNFT.sol +++ b/contracts/ConfigurableGuildRewardNFT.sol @@ -25,6 +25,7 @@ contract ConfigurableGuildRewardNFT is uint256 public constant SIGNATURE_VALIDITY = 1 hours; address public factoryProxy; + uint256 public maxSupply; uint256 public mintableAmountPerUser; /// @notice The cid for tokenURI. @@ -37,7 +38,10 @@ contract ConfigurableGuildRewardNFT is IGuildRewardNFTFactory.ConfigurableNFTConfig memory nftConfig, address factoryProxyAddress ) public initializer { + if (nftConfig.maxSupply <= 0) revert MaxSupplyZero(); + cid = nftConfig.cid; + maxSupply = nftConfig.maxSupply; mintableAmountPerUser = nftConfig.mintableAmountPerUser; factoryProxy = factoryProxyAddress; @@ -68,6 +72,8 @@ contract ConfigurableGuildRewardNFT is uint256 firstTokenId = totalSupply(); uint256 lastTokenId = firstTokenId + amount - 1; + if (lastTokenId >= maxSupply) revert MaxSupplyReached(maxSupply); + for (uint256 tokenId = firstTokenId; tokenId <= lastTokenId; ) { _safeMint(receiver, tokenId); @@ -115,6 +121,12 @@ contract ConfigurableGuildRewardNFT is else emit Unlocked(0); } + function setMaxSupply(uint256 newMaxSupply) external onlyOwner { + if (newMaxSupply <= 0) revert MaxSupplyZero(); + maxSupply = newMaxSupply; + emit MaxSupplyChanged(newMaxSupply); + } + function setMintableAmountPerUser(uint256 newAmount) external onlyOwner { mintableAmountPerUser = newAmount; emit MintableAmountPerUserChanged(newAmount); diff --git a/contracts/interfaces/IConfigurableGuildRewardNFT.sol b/contracts/interfaces/IConfigurableGuildRewardNFT.sol index 2b9c2dd..244b538 100644 --- a/contracts/interfaces/IConfigurableGuildRewardNFT.sol +++ b/contracts/interfaces/IConfigurableGuildRewardNFT.sol @@ -2,9 +2,10 @@ pragma solidity ^0.8.0; import { IGuildRewardNFTFactory } from "./IGuildRewardNFTFactory.sol"; +import { IMaxSupply } from "./IMaxSupply.sol"; /// @title An NFT distributed as a reward for Guild.xyz users. -interface IConfigurableGuildRewardNFT { +interface IConfigurableGuildRewardNFT is IMaxSupply { /// @notice The address of the proxy to be used when interacting with the factory. /// @dev Used to access the factory's address when interacting through minimal proxies. /// @return factoryAddress The address of the factory. diff --git a/contracts/interfaces/IGuildRewardNFTFactory.sol b/contracts/interfaces/IGuildRewardNFTFactory.sol index 5dc82e2..1633eb8 100644 --- a/contracts/interfaces/IGuildRewardNFTFactory.sol +++ b/contracts/interfaces/IGuildRewardNFTFactory.sol @@ -19,6 +19,7 @@ interface IGuildRewardNFTFactory { /// @param tokenTreasury The address that will collect the prices of the minted tokens. /// @param tokenFee The price of every mint in wei. /// @param soulbound Whether the token should be soulbound. + /// @param maxSupply The maximum number of tokens that users will ever be able to mint. /// @param mintableAmountPerUser The maximum amount a user will be able to mint from the deployed token. struct ConfigurableNFTConfig { string name; @@ -28,6 +29,7 @@ interface IGuildRewardNFTFactory { address payable treasury; uint256 tokenFee; bool soulbound; + uint256 maxSupply; uint256 mintableAmountPerUser; } diff --git a/contracts/interfaces/IMaxSupply.sol b/contracts/interfaces/IMaxSupply.sol new file mode 100644 index 0000000..7de9f13 --- /dev/null +++ b/contracts/interfaces/IMaxSupply.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IMaxSupply { + /// @notice The maximum number of tokens that can ever be minted. + /// @return count The number of tokens. + function maxSupply() external view returns (uint256 count); + + /// @notice Sets the maximum number of tokens that can ever be minted. + /// @dev Only callable by the owner. + /// @param newMaxSupply The number of tokens. + function setMaxSupply(uint256 newMaxSupply) external; + + /// @notice Event emitted when the maxSupply is changed. + /// @param newMaxSupply The number of tokens. + event MaxSupplyChanged(uint256 newMaxSupply); + + /// @notice Error thrown when the maximum supply attempted to be set is zero. + error MaxSupplyZero(); + + /// @notice Error thrown when the tokenId is higher than the maximum supply. + /// @param maxSupply The maximum supply of the token. + error MaxSupplyReached(uint256 maxSupply); +} diff --git a/docs/contracts/ConfigurableGuildRewardNFT.md b/docs/contracts/ConfigurableGuildRewardNFT.md index 695bfea..934e191 100644 --- a/docs/contracts/ConfigurableGuildRewardNFT.md +++ b/docs/contracts/ConfigurableGuildRewardNFT.md @@ -32,6 +32,19 @@ _Used to access the factory's address when interacting through minimal proxies._ | Name | Type | Description | | ---- | ---- | ----------- | +### maxSupply + +```solidity +uint256 maxSupply +``` + +The maximum number of tokens that can ever be minted. + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + ### mintableAmountPerUser ```solidity @@ -149,6 +162,24 @@ Only callable by the owner. | :--- | :--- | :---------- | | `newLocked` | bool | Whether the token should be soulbound or not. | +### setMaxSupply + +```solidity +function setMaxSupply( + uint256 newMaxSupply +) external +``` + +Sets the maximum number of tokens that can ever be minted. + +Only callable by the owner. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `newMaxSupply` | uint256 | The number of tokens. | + ### setMintableAmountPerUser ```solidity diff --git a/docs/contracts/interfaces/IGuildRewardNFTFactory.md b/docs/contracts/interfaces/IGuildRewardNFTFactory.md index e135687..dc42f3b 100644 --- a/docs/contracts/interfaces/IGuildRewardNFTFactory.md +++ b/docs/contracts/interfaces/IGuildRewardNFTFactory.md @@ -234,6 +234,7 @@ struct ConfigurableNFTConfig { address payable treasury; uint256 tokenFee; bool soulbound; + uint256 maxSupply; uint256 mintableAmountPerUser; } ``` diff --git a/docs/contracts/interfaces/IMaxSupply.md b/docs/contracts/interfaces/IMaxSupply.md new file mode 100644 index 0000000..7332007 --- /dev/null +++ b/docs/contracts/interfaces/IMaxSupply.md @@ -0,0 +1,77 @@ +# IMaxSupply + +## Functions + +### maxSupply + +```solidity +function maxSupply() external returns (uint256 count) +``` + +The maximum number of tokens that can ever be minted. + +#### Return Values + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `count` | uint256 | The number of tokens. | +### setMaxSupply + +```solidity +function setMaxSupply( + uint256 newMaxSupply +) external +``` + +Sets the maximum number of tokens that can ever be minted. + +Only callable by the owner. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `newMaxSupply` | uint256 | The number of tokens. | + +## Events + +### MaxSupplyChanged + +```solidity +event MaxSupplyChanged( + uint256 newMaxSupply +) +``` + +Event emitted when the maxSupply is changed. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `newMaxSupply` | uint256 | The number of tokens. | + +## Custom errors + +### MaxSupplyZero + +```solidity +error MaxSupplyZero() +``` + +Error thrown when the maximum supply attempted to be set is zero. + +### MaxSupplyReached + +```solidity +error MaxSupplyReached(uint256 maxSupply) +``` + +Error thrown when the tokenId is higher than the maximum supply. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| maxSupply | uint256 | The maximum supply of the token. | + diff --git a/scripts/deploy-configurable-nft-zksync.ts b/scripts/deploy-configurable-nft-zksync.ts index 0f1832c..f07add6 100644 --- a/scripts/deploy-configurable-nft-zksync.ts +++ b/scripts/deploy-configurable-nft-zksync.ts @@ -14,6 +14,7 @@ const nftConfig = { treasury: "0x...", // The address that will receive the price paid for mints. tokenFee: 0, // The price of every mint in wei. soulbound: true, // Whether the token should be soulbound or not. + maxSupply: 10, // The maximum number of tokens that users will ever be able to mint. mintableAmountPerUser: 1 // The maximum amount a user will be able to mint from the token. }; diff --git a/scripts/deploy-configurable-nft.ts b/scripts/deploy-configurable-nft.ts index 57fe13e..8b3a21f 100644 --- a/scripts/deploy-configurable-nft.ts +++ b/scripts/deploy-configurable-nft.ts @@ -11,6 +11,7 @@ const nftConfig = { treasury: "0x...", // The address that will receive the price paid for mints. tokenFee: 0, // The price of every mint in wei. soulbound: true, // Whether the token should be soulbound or not. + maxSupply: 10, // The maximum number of tokens that users will ever be able to mint. mintableAmountPerUser: 1 // The maximum amount a user will be able to mint from the token. }; diff --git a/test/ConfigurableGuildRewardNFT.spec.ts b/test/ConfigurableGuildRewardNFT.spec.ts index 6c36d9c..0eeda82 100644 --- a/test/ConfigurableGuildRewardNFT.spec.ts +++ b/test/ConfigurableGuildRewardNFT.spec.ts @@ -11,6 +11,7 @@ const baseNFTConfig = { cid: sampleCids[0], tokenFee: ethers.parseEther("0.1"), soulbound: true, + maxSupply: 10n, mintableAmountPerUser: 1n }; let nftConfig: typeof baseNFTConfig & { tokenOwner: string; treasury: string }; @@ -94,6 +95,7 @@ describe("ConfigurableGuildRewardNFT", () => { expect(await nft.name()).to.eq(nftConfig.name); expect(await nft.symbol()).to.eq(nftConfig.symbol); expect(await nft.owner()).to.eq(wallet0.address); + expect(await nft.maxSupply()).to.eq(nftConfig.maxSupply); expect(await nft.mintableAmountPerUser()).to.eq(nftConfig.mintableAmountPerUser); expect(await nft.factoryProxy()).to.eq(await factory.getAddress()); }); @@ -223,6 +225,31 @@ describe("ConfigurableGuildRewardNFT", () => { expect(userBalance1).to.eq(userBalance0 + sampleAmount); }); + it("should revert if the max supply is reached", async () => { + const newMaxSupply = sampleAmount; + await nft.setMaxSupply(newMaxSupply); + await nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignedAt, sampleSignature, { + value: fee + nftConfig.tokenFee + }); + + const signature = await createSignature( + signer, + sampleAmount, + randomWallet.address, + sampleUserId + 1, + sampleSignedAt, + chainId, + await nft.getAddress() + ); + await expect( + nft.claim(sampleAmount, randomWallet.address, sampleUserId + 1, sampleSignedAt, signature, { + value: fee + nftConfig.tokenFee + }) + ) + .to.be.revertedWithCustomError(nft, "MaxSupplyReached") + .withArgs(newMaxSupply); + }); + it("should mint the token", async () => { const totalSupply = await nft.totalSupply(); const tokenId = totalSupply; @@ -492,6 +519,30 @@ describe("ConfigurableGuildRewardNFT", () => { }); }); + context("#setMaxSupply", () => { + it("should revert if maxSupply is attempted to be changed by anyone but the owner", async () => { + await expect((nft.connect(randomWallet) as Contract).setMaxSupply(5)).to.be.revertedWith( + "Ownable: caller is not the owner" + ); + }); + + it("should revert if maxSupply is attempted to be set to 0", async () => { + await expect(nft.setMaxSupply(0)).to.be.revertedWithCustomError(nft, "MaxSupplyZero"); + }); + + it("should update maxSupply", async () => { + const maxSupply = 5; + await nft.setMaxSupply(maxSupply); + const newMaxSupply = await nft.maxSupply(); + expect(newMaxSupply).to.eq(maxSupply); + }); + + it("should emit MaxSupplyChanged event", async () => { + const maxSupply = 5; + await expect(nft.setMaxSupply(maxSupply)).to.emit(nft, "MaxSupplyChanged").withArgs(maxSupply); + }); + }); + context("#setMintableAmountPerUser", () => { it("should revert if mintableAmountPerUser is attempted to be changed by anyone but the owner", async () => { await expect((nft.connect(randomWallet) as Contract).setMintableAmountPerUser(5)).to.be.revertedWith( diff --git a/test/GuildRewardNFTFactory.spec.ts b/test/GuildRewardNFTFactory.spec.ts index 4b072ea..3267d73 100644 --- a/test/GuildRewardNFTFactory.spec.ts +++ b/test/GuildRewardNFTFactory.spec.ts @@ -10,6 +10,7 @@ const sampleSymbol = "TGP"; const cids = ["QmPaZD7i8TpLEeGjHtGoXe4mPKbRNNt8YTHH5nrKoqz9wJ", "QmcaGypWsmzaSQQGuExUjtyTRvZ2FF525Ww6PBNWWgkkLj"]; const sampleFee = 69; const sampleSoulbound = true; +const sampleMaxSupply = 10n; const sampleMintableAmountPerUser = 1; // CONTRACTS @@ -80,6 +81,7 @@ describe("GuildRewardNFTFactory", () => { treasury: treasury.address, tokenFee: sampleFee, soulbound: sampleSoulbound, + maxSupply: sampleMaxSupply, mintableAmountPerUser: sampleMintableAmountPerUser }); const nftAddresses = await factory.getDeployedTokenContracts(wallet0.address); @@ -88,6 +90,7 @@ describe("GuildRewardNFTFactory", () => { expect(await nft.symbol()).to.eq(sampleSymbol); expect(await nft.owner()).to.eq(randomWallet.address); expect(await nft.fee()).to.eq(sampleFee); + expect(await nft.maxSupply()).to.eq(sampleMaxSupply); expect(await nft.mintableAmountPerUser()).to.eq(sampleMintableAmountPerUser); });