From c008caf1044b79e397be76294a923a13c6fb6f8e Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Fri, 19 Apr 2024 17:16:38 +0200 Subject: [PATCH 01/18] Add contractType to RewardNFTDeployed event --- contracts/GuildRewardNFTFactory.sol | 7 ++++--- contracts/interfaces/IGuildRewardNFTFactory.sol | 3 ++- test/GuildRewardNFTFactory.spec.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/contracts/GuildRewardNFTFactory.sol b/contracts/GuildRewardNFTFactory.sol index 8a6410b..2eab671 100644 --- a/contracts/GuildRewardNFTFactory.sol +++ b/contracts/GuildRewardNFTFactory.sol @@ -39,16 +39,17 @@ contract GuildRewardNFTFactory is address payable tokenTreasury, uint256 tokenFee ) external { - address deployedCloneAddress = ClonesUpgradeable.clone(nftImplementations[ContractType.BASIC_NFT]); + ContractType contractType = ContractType.BASIC_NFT; + address deployedCloneAddress = ClonesUpgradeable.clone(nftImplementations[contractType]); IBasicGuildRewardNFT deployedClone = IBasicGuildRewardNFT(deployedCloneAddress); deployedClone.initialize(name, symbol, cid, tokenOwner, tokenTreasury, tokenFee, address(this)); deployedTokenContracts[msg.sender].push( - Deployment({ contractAddress: deployedCloneAddress, contractType: ContractType.BASIC_NFT }) + Deployment({ contractAddress: deployedCloneAddress, contractType: contractType }) ); - emit RewardNFTDeployed(msg.sender, deployedCloneAddress); + emit RewardNFTDeployed(msg.sender, deployedCloneAddress, contractType); } function setNFTImplementation(ContractType contractType, address newNFT) external onlyOwner { diff --git a/contracts/interfaces/IGuildRewardNFTFactory.sol b/contracts/interfaces/IGuildRewardNFTFactory.sol index 58e7fce..1d5e98a 100644 --- a/contracts/interfaces/IGuildRewardNFTFactory.sol +++ b/contracts/interfaces/IGuildRewardNFTFactory.sol @@ -72,7 +72,8 @@ interface IGuildRewardNFTFactory { /// @notice Event emitted when a new NFT is deployed. /// @param deployer The address that deployed the token. /// @param tokenAddress The address of the token. - event RewardNFTDeployed(address deployer, address tokenAddress); + /// @param contractType The type of the NFT deployed. + event RewardNFTDeployed(address deployer, address tokenAddress, ContractType contractType); /// @notice Event emitted when the validSigner is changed. /// @param newValidSigner The new address of validSigner. diff --git a/test/GuildRewardNFTFactory.spec.ts b/test/GuildRewardNFTFactory.spec.ts index 263161a..3821712 100644 --- a/test/GuildRewardNFTFactory.spec.ts +++ b/test/GuildRewardNFTFactory.spec.ts @@ -66,7 +66,7 @@ describe("GuildRewardNFTFactory", () => { it("should emit RewardNFTDeployed event", async () => { await expect(factory.deployBasicNFT(sampleName, sampleSymbol, cids[0], wallet0.address, treasury.address, 0)) .to.emit(factory, "RewardNFTDeployed") - .withArgs(wallet0.address, anyValue); + .withArgs(wallet0.address, anyValue, ContractType.BASIC_NFT); }); context("#setNFTImplementation", () => { From fa95cb34883c0d182e5fcf962c75cadf36c279a7 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Fri, 19 Apr 2024 17:39:16 +0200 Subject: [PATCH 02/18] Add ConfigurableGuildRewardNFT --- contracts/ConfigurableGuildRewardNFT.sol | 118 ++++++++++++++++++ .../IConfigurableGuildRewardNFT.sol | 72 +++++++++++ contracts/token/OptionallySoulboundERC721.sol | 113 +++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 contracts/ConfigurableGuildRewardNFT.sol create mode 100644 contracts/interfaces/IConfigurableGuildRewardNFT.sol create mode 100644 contracts/token/OptionallySoulboundERC721.sol diff --git a/contracts/ConfigurableGuildRewardNFT.sol b/contracts/ConfigurableGuildRewardNFT.sol new file mode 100644 index 0000000..5b9ac6c --- /dev/null +++ b/contracts/ConfigurableGuildRewardNFT.sol @@ -0,0 +1,118 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import { IConfigurableGuildRewardNFT } from "./interfaces/IConfigurableGuildRewardNFT.sol"; +import { IGuildRewardNFTFactory } from "./interfaces/IGuildRewardNFTFactory.sol"; +import { ITreasuryManager } from "./interfaces/ITreasuryManager.sol"; +import { LibTransfer } from "./lib/LibTransfer.sol"; +import { OptionallySoulboundERC721 } from "./token/OptionallySoulboundERC721.sol"; +import { TreasuryManager } from "./utils/TreasuryManager.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/// @title An NFT distributed as a reward for Guild.xyz users. +contract ConfigurableGuildRewardNFT is + IConfigurableGuildRewardNFT, + Initializable, + OwnableUpgradeable, + OptionallySoulboundERC721, + TreasuryManager +{ + using ECDSA for bytes32; + using LibTransfer for address; + using LibTransfer for address payable; + + address public factoryProxy; + uint256 public mintableAmountPerUser; + + /// @notice The cid for tokenURI. + string internal cid; + + /// @notice The number of claimed tokens by userIds. + mapping(uint256 userId => uint256 claimed) internal claimedTokens; + + function initialize( + IGuildRewardNFTFactory.ConfigurableNFTConfig memory nftConfig, + address factoryProxyAddress + ) public initializer { + cid = nftConfig.cid; + mintableAmountPerUser = nftConfig.mintableAmountPerUser; + factoryProxy = factoryProxyAddress; + + __OptionallySoulboundERC721_init(nftConfig.name, nftConfig.symbol, nftConfig.soulbound); + __TreasuryManager_init(nftConfig.treasury, nftConfig.tokenFee); + + _transferOwnership(nftConfig.tokenOwner); + } + + function claim(uint256 amount, address receiver, uint256 userId, bytes calldata signature) external payable { + if ( + amount > mintableAmountPerUser - balanceOf(receiver) || + amount > mintableAmountPerUser - claimedTokens[userId] + ) revert AlreadyClaimed(); + if (!isValidSignature(amount, receiver, userId, signature)) revert IncorrectSignature(); + + (uint256 guildFee, address payable guildTreasury) = ITreasuryManager(factoryProxy).getFeeData(); + + claimedTokens[userId] += amount; + + uint256 firstTokenId = totalSupply(); + uint256 lastTokenId = firstTokenId + amount - 1; + + for (uint256 tokenId = firstTokenId; tokenId <= lastTokenId; ++tokenId) { + _safeMint(receiver, tokenId); + + if (soulbound) emit Locked(tokenId); + + emit Claimed(receiver, tokenId); + } + + // Fee collection + if (msg.value == guildFee + fee) { + guildTreasury.sendEther(amount * guildFee); + treasury.sendEther(amount * fee); + } else revert IncorrectFee(msg.value, amount * (guildFee + fee)); + } + + function burn(uint256[] calldata tokenIds, uint256 userId, bytes calldata signature) external { + uint256 amount = tokenIds.length; + if (!isValidSignature(amount, msg.sender, userId, signature)) revert IncorrectSignature(); + + for (uint256 i; i < amount; ++i) { + uint256 tokenId = tokenIds[i]; + + if (msg.sender != ownerOf(tokenId)) revert IncorrectSender(); + claimedTokens[userId]--; + _burn(tokenId); + } + } + + function updateTokenURI(string calldata newCid) external onlyOwner { + cid = newCid; + emit MetadataUpdate(); + } + + function balanceOf(uint256 userId) external view returns (uint256 amount) { + return claimedTokens[userId]; + } + + function tokenURI(uint256 tokenId) public view override returns (string memory) { + if (!_exists(tokenId)) revert NonExistentToken(tokenId); + + return string.concat("ipfs://", cid); + } + + /// @notice Checks the validity of the signature for the given params. + function isValidSignature( + uint256 amount, + address receiver, + uint256 userId, + bytes calldata signature + ) internal view returns (bool) { + if (signature.length != 65) revert IncorrectSignature(); + bytes32 message = keccak256(abi.encode(amount, receiver, userId, block.chainid, address(this))) + .toEthSignedMessageHash(); + return message.recover(signature) == IGuildRewardNFTFactory(factoryProxy).validSigner(); + } +} diff --git a/contracts/interfaces/IConfigurableGuildRewardNFT.sol b/contracts/interfaces/IConfigurableGuildRewardNFT.sol new file mode 100644 index 0000000..0aa2e1b --- /dev/null +++ b/contracts/interfaces/IConfigurableGuildRewardNFT.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { IGuildRewardNFTFactory } from "./IGuildRewardNFTFactory.sol"; + +/// @title An NFT distributed as a reward for Guild.xyz users. +interface IConfigurableGuildRewardNFT { + /// @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. + function factoryProxy() external view returns (address factoryAddress); + + /// @notice The maximum amount of tokens a Guild user can claim from the token. + /// @dev Doesn't matter if they are claimed in the same transaction or separately. + /// @return mintableAmountPerUser The amount of tokens. + function mintableAmountPerUser() external view returns (uint256 mintableAmountPerUser); + + /// @notice Returns the number of tokens the user claimed. + /// @dev Analogous to balanceOf(address), but works with Guild user ids. + /// @param userId The id of the user on Guild. + /// @return amount The number of tokens the userId has claimed. + function balanceOf(uint256 userId) external view returns (uint256 amount); + + /// @notice Sets metadata and the associated addresses. + /// @dev Initializer function callable only once. + /// @param nftConfig See struct ConfigurableNFTConfig in IGuildRewardNFTFactory. + /// @param factoryProxyAddress The address of the factory. + function initialize( + IGuildRewardNFTFactory.ConfigurableNFTConfig memory nftConfig, + address factoryProxyAddress + ) external; + + /// @notice Claims tokens to the given address. + /// @param amount The amount of tokens to mint. Should be less or equal to mintableAmountPerUser. + /// @param receiver The address that receives the token. + /// @param userId The id of the user on Guild. + /// @param signature The following signed by validSigner: amount, receiver, userId, chainId, the contract's address. + function claim(uint256 amount, address receiver, uint256 userId, bytes calldata signature) external payable; + + /// @notice Burns tokens from the sender. + /// @param tokenIds The tokenIds to burn. All of them should belong to userId. + /// @param userId The id of the user on Guild. + /// @param signature The following signed by validSigner: amount, receiver, userId, chainId, the contract's address. + function burn(uint256[] calldata tokenIds, uint256 userId, bytes calldata signature) external; + + /// @notice Updates the cid for tokenURI. + /// @dev Only callable by the owner. + /// @param newCid The new cid that points to the updated image. + function updateTokenURI(string calldata newCid) external; + + /// @notice Event emitted whenever a claim succeeds. + /// @param receiver The address that received the tokens. + /// @param tokenId The id of the token. + event Claimed(address indexed receiver, uint256 tokenId); + + /// @notice Event emitted whenever the cid is updated. + event MetadataUpdate(); + + /// @notice Error thrown when the token is already claimed. + error AlreadyClaimed(); + + /// @notice Error thrown when an incorrect amount of fee is attempted to be paid. + /// @param paid The amount of funds received. + /// @param requiredAmount The amount of fees required for claiming a single token. + error IncorrectFee(uint256 paid, uint256 requiredAmount); + + /// @notice Error thrown when the sender is not permitted to do a specific action. + error IncorrectSender(); + + /// @notice Error thrown when the supplied signature is invalid. + error IncorrectSignature(); +} diff --git a/contracts/token/OptionallySoulboundERC721.sol b/contracts/token/OptionallySoulboundERC721.sol new file mode 100644 index 0000000..8eb15d4 --- /dev/null +++ b/contracts/token/OptionallySoulboundERC721.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +/* solhint-disable max-line-length */ + +import { IERC5192 } from "../interfaces/IERC5192.sol"; +import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import { IERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import { ERC721EnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; +import { IERC721EnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/IERC721EnumerableUpgradeable.sol"; + +/* solhint-enable max-line-length */ + +/// @title An enumerable ERC721 that's optionally soulbound. +/// @notice Allowance and transfer-related functions are disabled in soulbound mode. +/// @dev Inheriting from upgradeable contracts here - even though we're using it in a non-upgradeable way, +/// we still want it to be initializable +contract OptionallySoulboundERC721 is ERC721Upgradeable, ERC721EnumerableUpgradeable, IERC5192 { + /// @notice Whether the token is set as soulbound. + bool internal soulbound; + + /// @notice Error thrown when trying to query info about a token that's not (yet) minted. + /// @param tokenId The queried id. + error NonExistentToken(uint256 tokenId); + + /// @notice Error thrown when a function's execution is not possible, because the soulbound mode is on. + error Soulbound(); + + // solhint-disable-next-line func-name-mixedcase + function __OptionallySoulboundERC721_init( + string memory name_, + string memory symbol_, + bool soulbound_ + ) internal onlyInitializing { + soulbound = soulbound_; + __ERC721_init(name_, symbol_); + __ERC721Enumerable_init(); + } + + /// @inheritdoc ERC721EnumerableUpgradeable + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721EnumerableUpgradeable, ERC721Upgradeable) returns (bool) { + return + interfaceId == type(IERC5192).interfaceId || + interfaceId == type(IERC721EnumerableUpgradeable).interfaceId || + super.supportsInterface(interfaceId); + } + + function locked(uint256 tokenId) external view returns (bool) { + if (!_exists(tokenId)) revert NonExistentToken(tokenId); + return soulbound; + } + + function approve(address to, uint256 tokenId) public virtual override(IERC721Upgradeable, ERC721Upgradeable) { + if (soulbound) revert Soulbound(); + super.approve(to, tokenId); + } + + function setApprovalForAll( + address operator, + bool approved + ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) { + if (soulbound) revert Soulbound(); + super.setApprovalForAll(operator, approved); + } + + function isApprovedForAll( + address owner, + address operator + ) public view virtual override(IERC721Upgradeable, ERC721Upgradeable) returns (bool) { + if (soulbound) revert Soulbound(); + return super.isApprovedForAll(owner, operator); + } + + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) { + if (soulbound) revert Soulbound(); + super.transferFrom(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) { + if (soulbound) revert Soulbound(); + super.safeTransferFrom(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) { + if (soulbound) revert Soulbound(); + super.safeTransferFrom(from, to, tokenId, data); + } + + /// @dev Used for minting/burning even when soulbound. + function _beforeTokenTransfer( + address from, + address to, + uint256 firstTokenId, + uint256 batchSize + ) internal virtual override(ERC721EnumerableUpgradeable, ERC721Upgradeable) { + super._beforeTokenTransfer(from, to, firstTokenId, batchSize); + } +} From d67d23dbc99fd6d869362387989b9df6287f44c7 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Fri, 19 Apr 2024 17:40:04 +0200 Subject: [PATCH 03/18] Add function to factory that deploys configurable NFTs --- contracts/GuildRewardNFTFactory.sol | 15 ++++++++++ .../interfaces/IGuildRewardNFTFactory.sol | 28 ++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/contracts/GuildRewardNFTFactory.sol b/contracts/GuildRewardNFTFactory.sol index 2eab671..f6289b6 100644 --- a/contracts/GuildRewardNFTFactory.sol +++ b/contracts/GuildRewardNFTFactory.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.19; import { IBasicGuildRewardNFT } from "./interfaces/IBasicGuildRewardNFT.sol"; +import { IConfigurableGuildRewardNFT } from "./interfaces/IConfigurableGuildRewardNFT.sol"; import { IGuildRewardNFTFactory } from "./interfaces/IGuildRewardNFTFactory.sol"; import { TreasuryManager } from "./utils/TreasuryManager.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -52,6 +53,20 @@ contract GuildRewardNFTFactory is emit RewardNFTDeployed(msg.sender, deployedCloneAddress, contractType); } + function deployConfigurableNFT(ConfigurableNFTConfig memory nftConfig) external { + ContractType contractType = ContractType.CONFIGURABLE_NFT; + address deployedCloneAddress = ClonesUpgradeable.clone(nftImplementations[contractType]); + IConfigurableGuildRewardNFT deployedClone = IConfigurableGuildRewardNFT(deployedCloneAddress); + + deployedClone.initialize(nftConfig, address(this)); + + deployedTokenContracts[msg.sender].push( + Deployment({ contractAddress: deployedCloneAddress, contractType: contractType }) + ); + + emit RewardNFTDeployed(msg.sender, deployedCloneAddress, contractType); + } + function setNFTImplementation(ContractType contractType, address newNFT) external onlyOwner { nftImplementations[contractType] = newNFT; emit ImplementationChanged(contractType, newNFT); diff --git a/contracts/interfaces/IGuildRewardNFTFactory.sol b/contracts/interfaces/IGuildRewardNFTFactory.sol index 1d5e98a..430f5a4 100644 --- a/contracts/interfaces/IGuildRewardNFTFactory.sol +++ b/contracts/interfaces/IGuildRewardNFTFactory.sol @@ -6,7 +6,29 @@ interface IGuildRewardNFTFactory { /// @notice The type of the contract. /// @dev Used as an identifier. Should be expanded in future updates. enum ContractType { - BASIC_NFT + BASIC_NFT, + CONFIGURABLE_NFT + } + + /// @notice Input parameters of the deployConfigurableNFT function. + /// @dev Needed to prevent "stack too deep" errors. + /// @param name The name of the NFT to be created. + /// @param symbol The symbol of the NFT to be created. + /// @param cid The cid used to construct the tokenURI of the NFT to be created. + /// @param tokenOwner The address that will be the owner of the deployed token. + /// @param tokenTreasury The address that will collect the prices of the minted deployed tokens. + /// @param tokenFee The price of every mint in wei. + /// @param soulbound Whether the token should be soulbound. + /// @param mintableAmountPerUser The maximum amount a user will be able to mint from the deployed token. + struct ConfigurableNFTConfig { + string name; + string symbol; + string cid; + address tokenOwner; + address payable treasury; + uint256 tokenFee; + bool soulbound; + uint256 mintableAmountPerUser; } /// @notice Information about a specific deployment. @@ -48,6 +70,10 @@ interface IGuildRewardNFTFactory { uint256 tokenFee ) external; + /// @notice Deploys a minimal proxy for a configurable NFT. + /// @param nftConfig The config to initialize the token to be deployed with. + function deployConfigurableNFT(ConfigurableNFTConfig memory nftConfig) external; + /// @notice Returns the reward NFT addresses for a guild. /// @param deployer The address that deployed the tokens. /// @return tokens The addresses of the tokens deployed by deployer. From 4ecf4fbd4277742bde3f552f25d5f2c1b87dbe63 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Fri, 19 Apr 2024 17:40:19 +0200 Subject: [PATCH 04/18] Unify comments --- contracts/interfaces/IBasicGuildRewardNFT.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/interfaces/IBasicGuildRewardNFT.sol b/contracts/interfaces/IBasicGuildRewardNFT.sol index fd6e076..20127fb 100644 --- a/contracts/interfaces/IBasicGuildRewardNFT.sol +++ b/contracts/interfaces/IBasicGuildRewardNFT.sol @@ -13,8 +13,8 @@ interface IBasicGuildRewardNFT { /// @return claimed Whether the address has claimed their token. function hasClaimed(address account) external view returns (bool claimed); - /// @notice Whether a userId has minted a token. - /// @dev Used to prevent double mints in the same block. + /// @notice Whether a userId has claimed a token. + /// @dev Used to prevent double claims in the same block. /// @param userId The id of the user on Guild. /// @return claimed Whether the userId has claimed any tokens. function hasTheUserIdClaimed(uint256 userId) external view returns (bool claimed); @@ -23,10 +23,10 @@ interface IBasicGuildRewardNFT { /// @dev Initializer function callable only once. /// @param name The name of the token. /// @param symbol The symbol of the token. - /// @param cid The cid used to construct the tokenURI for the token to be minted. - /// @param tokenOwner The address that will be the owner of the deployed token. + /// @param cid The cid used to construct the tokenURI for the token to be deployed. + /// @param tokenOwner The address that will be the owner of the token. /// @param treasury The address that will receive the price paid for the token. - /// @param tokenFee The price of every mint in wei. + /// @param tokenFee The price of every claim in wei. /// @param factoryProxyAddress The address of the factory. function initialize( string memory name, @@ -68,7 +68,7 @@ interface IBasicGuildRewardNFT { /// @notice Error thrown when an incorrect amount of fee is attempted to be paid. /// @param paid The amount of funds received. - /// @param requiredAmount The amount of fees required for minting. + /// @param requiredAmount The amount of fees required for claiming. error IncorrectFee(uint256 paid, uint256 requiredAmount); /// @notice Error thrown when the sender is not permitted to do a specific action. From de9aab1178365e14f2402bc588fb7954286db375 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Fri, 19 Apr 2024 17:42:47 +0200 Subject: [PATCH 05/18] Update docs --- docs/contracts/BasicGuildRewardNFT.md | 4 +- docs/contracts/ConfigurableGuildRewardNFT.md | 195 +++++++++++++++ docs/contracts/GuildRewardNFTFactory.md | 16 ++ .../interfaces/IBasicGuildRewardNFT.md | 12 +- .../interfaces/IConfigurableGuildRewardNFT.md | 208 ++++++++++++++++ .../interfaces/IGuildRewardNFTFactory.md | 37 ++- .../token/OptionallySoulboundERC721.md | 224 ++++++++++++++++++ 7 files changed, 686 insertions(+), 10 deletions(-) create mode 100644 docs/contracts/ConfigurableGuildRewardNFT.md create mode 100644 docs/contracts/interfaces/IConfigurableGuildRewardNFT.md create mode 100644 docs/contracts/token/OptionallySoulboundERC721.md diff --git a/docs/contracts/BasicGuildRewardNFT.md b/docs/contracts/BasicGuildRewardNFT.md index 22f1f6d..dd9d86f 100644 --- a/docs/contracts/BasicGuildRewardNFT.md +++ b/docs/contracts/BasicGuildRewardNFT.md @@ -150,9 +150,9 @@ function hasTheUserIdClaimed( ) external returns (bool claimed) ``` -Whether a userId has minted a token. +Whether a userId has claimed a token. -Used to prevent double mints in the same block. +Used to prevent double claims in the same block. #### Parameters diff --git a/docs/contracts/ConfigurableGuildRewardNFT.md b/docs/contracts/ConfigurableGuildRewardNFT.md new file mode 100644 index 0000000..9cfecf5 --- /dev/null +++ b/docs/contracts/ConfigurableGuildRewardNFT.md @@ -0,0 +1,195 @@ +# ConfigurableGuildRewardNFT + +An NFT distributed as a reward for Guild.xyz users. + +## Variables + +### factoryProxy + +```solidity +address factoryProxy +``` + +The address of the proxy to be used when interacting with the factory. + +_Used to access the factory's address when interacting through minimal proxies._ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### mintableAmountPerUser + +```solidity +uint256 mintableAmountPerUser +``` + +The maximum amount of tokens a Guild user can claim from the token. + +_Doesn't matter if they are claimed in the same transaction or separately._ + +#### Return Values + +| Name | Type | Description | +| ---- | ---- | ----------- | + +### cid + +```solidity +string cid +``` + +The cid for tokenURI. + +### claimedTokens + +```solidity +mapping(uint256 => uint256) claimedTokens +``` + +The number of claimed tokens by userIds. + +## Functions + +### initialize + +```solidity +function initialize( + struct IGuildRewardNFTFactory.ConfigurableNFTConfig nftConfig, + address factoryProxyAddress +) public +``` + +Sets metadata and the associated addresses. + +Initializer function callable only once. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `nftConfig` | struct IGuildRewardNFTFactory.ConfigurableNFTConfig | See struct ConfigurableNFTConfig in IGuildRewardNFTFactory. | +| `factoryProxyAddress` | address | The address of the factory. | + +### claim + +```solidity +function claim( + uint256 amount, + address receiver, + uint256 userId, + bytes signature +) external +``` + +Claims tokens to the given address. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `amount` | uint256 | The amount of tokens to mint. Should be less or equal to mintableAmountPerUser. | +| `receiver` | address | The address that receives the token. | +| `userId` | uint256 | The id of the user on Guild. | +| `signature` | bytes | The following signed by validSigner: amount, receiver, userId, chainId, the contract's address. | + +### burn + +```solidity +function burn( + uint256[] tokenIds, + uint256 userId, + bytes signature +) external +``` + +Burns tokens from the sender. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `tokenIds` | uint256[] | The tokenIds to burn. All of them should belong to userId. | +| `userId` | uint256 | The id of the user on Guild. | +| `signature` | bytes | The following signed by validSigner: amount, receiver, userId, chainId, the contract's address. | + +### updateTokenURI + +```solidity +function updateTokenURI( + string newCid +) external +``` + +Updates the cid for tokenURI. + +Only callable by the owner. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `newCid` | string | The new cid that points to the updated image. | + +### balanceOf + +```solidity +function balanceOf( + uint256 userId +) external returns (uint256 amount) +``` + +Returns the number of tokens the user claimed. + +Analogous to balanceOf(address), but works with Guild user ids. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `userId` | uint256 | The id of the user on Guild. | + +#### Return Values + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `amount` | uint256 | The number of tokens the userId has claimed. | +### tokenURI + +```solidity +function tokenURI( + uint256 tokenId +) public returns (string) +``` + +See {IERC721Metadata-tokenURI}. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `tokenId` | uint256 | | + +### isValidSignature + +```solidity +function isValidSignature( + uint256 amount, + address receiver, + uint256 userId, + bytes signature +) internal returns (bool) +``` + +Checks the validity of the signature for the given params. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `amount` | uint256 | | +| `receiver` | address | | +| `userId` | uint256 | | +| `signature` | bytes | | + diff --git a/docs/contracts/GuildRewardNFTFactory.md b/docs/contracts/GuildRewardNFTFactory.md index d8ba5b2..b70df0e 100644 --- a/docs/contracts/GuildRewardNFTFactory.md +++ b/docs/contracts/GuildRewardNFTFactory.md @@ -89,6 +89,22 @@ Deploys a minimal proxy for a basic NFT. | `tokenTreasury` | address payable | The address that will collect the prices of the minted deployed tokens. | | `tokenFee` | uint256 | The price of every mint in wei. | +### deployConfigurableNFT + +```solidity +function deployConfigurableNFT( + struct IGuildRewardNFTFactory.ConfigurableNFTConfig nftConfig +) external +``` + +Deploys a minimal proxy for a configurable NFT. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `nftConfig` | struct IGuildRewardNFTFactory.ConfigurableNFTConfig | The config to initialize the token to be deployed with. | + ### setNFTImplementation ```solidity diff --git a/docs/contracts/interfaces/IBasicGuildRewardNFT.md b/docs/contracts/interfaces/IBasicGuildRewardNFT.md index c366281..6002f15 100644 --- a/docs/contracts/interfaces/IBasicGuildRewardNFT.md +++ b/docs/contracts/interfaces/IBasicGuildRewardNFT.md @@ -48,9 +48,9 @@ function hasTheUserIdClaimed( ) external returns (bool claimed) ``` -Whether a userId has minted a token. +Whether a userId has claimed a token. -Used to prevent double mints in the same block. +Used to prevent double claims in the same block. #### Parameters @@ -87,10 +87,10 @@ Initializer function callable only once. | :--- | :--- | :---------- | | `name` | string | The name of the token. | | `symbol` | string | The symbol of the token. | -| `cid` | string | The cid used to construct the tokenURI for the token to be minted. | -| `tokenOwner` | address | The address that will be the owner of the deployed token. | +| `cid` | string | The cid used to construct the tokenURI for the token to be deployed. | +| `tokenOwner` | address | The address that will be the owner of the token. | | `treasury` | address payable | The address that will receive the price paid for the token. | -| `tokenFee` | uint256 | The price of every mint in wei. | +| `tokenFee` | uint256 | The price of every claim in wei. | | `factoryProxyAddress` | address | The address of the factory. | ### claim @@ -202,7 +202,7 @@ Error thrown when an incorrect amount of fee is attempted to be paid. | Name | Type | Description | | ---- | ---- | ----------- | | paid | uint256 | The amount of funds received. | -| requiredAmount | uint256 | The amount of fees required for minting. | +| requiredAmount | uint256 | The amount of fees required for claiming. | ### IncorrectSender diff --git a/docs/contracts/interfaces/IConfigurableGuildRewardNFT.md b/docs/contracts/interfaces/IConfigurableGuildRewardNFT.md new file mode 100644 index 0000000..2c69dd7 --- /dev/null +++ b/docs/contracts/interfaces/IConfigurableGuildRewardNFT.md @@ -0,0 +1,208 @@ +# IConfigurableGuildRewardNFT + +An NFT distributed as a reward for Guild.xyz users. + +## Functions + +### factoryProxy + +```solidity +function factoryProxy() external returns (address factoryAddress) +``` + +The address of the proxy to be used when interacting with the factory. + +Used to access the factory's address when interacting through minimal proxies. + +#### Return Values + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `factoryAddress` | address | The address of the factory. | +### mintableAmountPerUser + +```solidity +function mintableAmountPerUser() external returns (uint256 mintableAmountPerUser) +``` + +The maximum amount of tokens a Guild user can claim from the token. + +Doesn't matter if they are claimed in the same transaction or separately. + +#### Return Values + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `mintableAmountPerUser` | uint256 | The amount of tokens. | +### balanceOf + +```solidity +function balanceOf( + uint256 userId +) external returns (uint256 amount) +``` + +Returns the number of tokens the user claimed. + +Analogous to balanceOf(address), but works with Guild user ids. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `userId` | uint256 | The id of the user on Guild. | + +#### Return Values + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `amount` | uint256 | The number of tokens the userId has claimed. | +### initialize + +```solidity +function initialize( + struct IGuildRewardNFTFactory.ConfigurableNFTConfig nftConfig, + address factoryProxyAddress +) external +``` + +Sets metadata and the associated addresses. + +Initializer function callable only once. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `nftConfig` | struct IGuildRewardNFTFactory.ConfigurableNFTConfig | See struct ConfigurableNFTConfig in IGuildRewardNFTFactory. | +| `factoryProxyAddress` | address | The address of the factory. | + +### claim + +```solidity +function claim( + uint256 amount, + address receiver, + uint256 userId, + bytes signature +) external +``` + +Claims tokens to the given address. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `amount` | uint256 | The amount of tokens to mint. Should be less or equal to mintableAmountPerUser. | +| `receiver` | address | The address that receives the token. | +| `userId` | uint256 | The id of the user on Guild. | +| `signature` | bytes | The following signed by validSigner: amount, receiver, userId, chainId, the contract's address. | + +### burn + +```solidity +function burn( + uint256[] tokenIds, + uint256 userId, + bytes signature +) external +``` + +Burns tokens from the sender. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `tokenIds` | uint256[] | The tokenIds to burn. All of them should belong to userId. | +| `userId` | uint256 | The id of the user on Guild. | +| `signature` | bytes | The following signed by validSigner: amount, receiver, userId, chainId, the contract's address. | + +### updateTokenURI + +```solidity +function updateTokenURI( + string newCid +) external +``` + +Updates the cid for tokenURI. + +Only callable by the owner. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `newCid` | string | The new cid that points to the updated image. | + +## Events + +### Claimed + +```solidity +event Claimed( + address receiver, + uint256 tokenId +) +``` + +Event emitted whenever a claim succeeds. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `receiver` | address | The address that received the tokens. | +| `tokenId` | uint256 | The id of the token. | +### MetadataUpdate + +```solidity +event MetadataUpdate( +) +``` + +Event emitted whenever the cid is updated. + +## Custom errors + +### AlreadyClaimed + +```solidity +error AlreadyClaimed() +``` + +Error thrown when the token is already claimed. + +### IncorrectFee + +```solidity +error IncorrectFee(uint256 paid, uint256 requiredAmount) +``` + +Error thrown when an incorrect amount of fee is attempted to be paid. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| paid | uint256 | The amount of funds received. | +| requiredAmount | uint256 | The amount of fees required for claiming a single token. | + +### IncorrectSender + +```solidity +error IncorrectSender() +``` + +Error thrown when the sender is not permitted to do a specific action. + +### IncorrectSignature + +```solidity +error IncorrectSignature() +``` + +Error thrown when the supplied signature is invalid. + diff --git a/docs/contracts/interfaces/IGuildRewardNFTFactory.md b/docs/contracts/interfaces/IGuildRewardNFTFactory.md index e27f03c..885b53f 100644 --- a/docs/contracts/interfaces/IGuildRewardNFTFactory.md +++ b/docs/contracts/interfaces/IGuildRewardNFTFactory.md @@ -84,6 +84,22 @@ Deploys a minimal proxy for a basic NFT. | `tokenTreasury` | address payable | The address that will collect the prices of the minted deployed tokens. | | `tokenFee` | uint256 | The price of every mint in wei. | +### deployConfigurableNFT + +```solidity +function deployConfigurableNFT( + struct IGuildRewardNFTFactory.ConfigurableNFTConfig nftConfig +) external +``` + +Deploys a minimal proxy for a configurable NFT. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `nftConfig` | struct IGuildRewardNFTFactory.ConfigurableNFTConfig | The config to initialize the token to be deployed with. | + ### getDeployedTokenContracts ```solidity @@ -167,7 +183,8 @@ Event emitted when an NFT implementation is changed. ```solidity event RewardNFTDeployed( address deployer, - address tokenAddress + address tokenAddress, + enum IGuildRewardNFTFactory.ContractType contractType ) ``` @@ -179,6 +196,7 @@ Event emitted when a new NFT is deployed. | :--- | :--- | :---------- | | `deployer` | address | The address that deployed the token. | | `tokenAddress` | address | The address of the token. | +| `contractType` | enum IGuildRewardNFTFactory.ContractType | The type of the NFT deployed. | ### ValidSignerChanged ```solidity @@ -201,7 +219,22 @@ Event emitted when the validSigner is changed. ```solidity enum ContractType { - BASIC_NFT + BASIC_NFT, + CONFIGURABLE_NFT +} +``` +### ConfigurableNFTConfig + +```solidity +struct ConfigurableNFTConfig { + string name; + string symbol; + string cid; + address tokenOwner; + address payable treasury; + uint256 tokenFee; + bool soulbound; + uint256 mintableAmountPerUser; } ``` ### Deployment diff --git a/docs/contracts/token/OptionallySoulboundERC721.md b/docs/contracts/token/OptionallySoulboundERC721.md new file mode 100644 index 0000000..5dbe59d --- /dev/null +++ b/docs/contracts/token/OptionallySoulboundERC721.md @@ -0,0 +1,224 @@ +# OptionallySoulboundERC721 + +An enumerable ERC721 that's optionally soulbound. + +Allowance and transfer-related functions are disabled in soulbound mode. + +Inheriting from upgradeable contracts here - even though we're using it in a non-upgradeable way, +we still want it to be initializable + +## Variables + +### soulbound + +```solidity +bool soulbound +``` + +Whether the token is set as soulbound. + +## Functions + +### __OptionallySoulboundERC721_init + +```solidity +function __OptionallySoulboundERC721_init( + string name_, + string symbol_, + bool soulbound_ +) internal +``` + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `name_` | string | | +| `symbol_` | string | | +| `soulbound_` | bool | | + +### supportsInterface + +```solidity +function supportsInterface( + bytes4 interfaceId +) public returns (bool) +``` + +See {IERC165-supportsInterface}. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `interfaceId` | bytes4 | | + +### locked + +```solidity +function locked( + uint256 tokenId +) external returns (bool) +``` + +Returns the locking status of an Soulbound Token + +SBTs assigned to zero address are considered invalid, and queries +about them do throw. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `tokenId` | uint256 | The identifier for an SBT. | + +### approve + +```solidity +function approve( + address to, + uint256 tokenId +) public +``` + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `to` | address | | +| `tokenId` | uint256 | | + +### setApprovalForAll + +```solidity +function setApprovalForAll( + address operator, + bool approved +) public +``` + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `operator` | address | | +| `approved` | bool | | + +### isApprovedForAll + +```solidity +function isApprovedForAll( + address owner, + address operator +) public returns (bool) +``` + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `owner` | address | | +| `operator` | address | | + +### transferFrom + +```solidity +function transferFrom( + address from, + address to, + uint256 tokenId +) public +``` + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `from` | address | | +| `to` | address | | +| `tokenId` | uint256 | | + +### safeTransferFrom + +```solidity +function safeTransferFrom( + address from, + address to, + uint256 tokenId +) public +``` + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `from` | address | | +| `to` | address | | +| `tokenId` | uint256 | | + +### safeTransferFrom + +```solidity +function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes data +) public +``` + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `from` | address | | +| `to` | address | | +| `tokenId` | uint256 | | +| `data` | bytes | | + +### _beforeTokenTransfer + +```solidity +function _beforeTokenTransfer( + address from, + address to, + uint256 firstTokenId, + uint256 batchSize +) internal +``` + +Used for minting/burning even when soulbound. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `from` | address | | +| `to` | address | | +| `firstTokenId` | uint256 | | +| `batchSize` | uint256 | | + +## Custom errors + +### NonExistentToken + +```solidity +error NonExistentToken(uint256 tokenId) +``` + +Error thrown when trying to query info about a token that's not (yet) minted. + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| tokenId | uint256 | The queried id. | + +### Soulbound + +```solidity +error Soulbound() +``` + +Error thrown when a function's execution is not possible, because the soulbound mode is on. + From aa753388b87b3d45de98f31ebfea3fffda56cb02 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Mon, 22 Apr 2024 21:37:14 +0200 Subject: [PATCH 06/18] Add configuration functions for owner --- contracts/ConfigurableGuildRewardNFT.sol | 11 ++++ .../IConfigurableGuildRewardNFT.sol | 14 +++++ docs/contracts/ConfigurableGuildRewardNFT.md | 36 +++++++++++++ .../interfaces/IConfigurableGuildRewardNFT.md | 52 +++++++++++++++++++ 4 files changed, 113 insertions(+) diff --git a/contracts/ConfigurableGuildRewardNFT.sol b/contracts/ConfigurableGuildRewardNFT.sol index 5b9ac6c..7b26093 100644 --- a/contracts/ConfigurableGuildRewardNFT.sol +++ b/contracts/ConfigurableGuildRewardNFT.sol @@ -88,6 +88,17 @@ contract ConfigurableGuildRewardNFT is } } + function setLocked(bool newLocked) external onlyOwner { + soulbound = newLocked; + if (newLocked) emit Locked(0); + else emit Unlocked(0); + } + + function setMintableAmountPerUser(uint256 newAmount) external onlyOwner { + mintableAmountPerUser = newAmount; + emit MintableAmountPerUserChanged(newAmount); + } + function updateTokenURI(string calldata newCid) external onlyOwner { cid = newCid; emit MetadataUpdate(); diff --git a/contracts/interfaces/IConfigurableGuildRewardNFT.sol b/contracts/interfaces/IConfigurableGuildRewardNFT.sol index 0aa2e1b..27bc637 100644 --- a/contracts/interfaces/IConfigurableGuildRewardNFT.sol +++ b/contracts/interfaces/IConfigurableGuildRewardNFT.sol @@ -43,6 +43,16 @@ interface IConfigurableGuildRewardNFT { /// @param signature The following signed by validSigner: amount, receiver, userId, chainId, the contract's address. function burn(uint256[] calldata tokenIds, uint256 userId, bytes calldata signature) external; + /// Sets the locked (i.e. soulboundness) status of all of the tokens in this NFT. + /// @dev Only callable by the owner. + /// @param newLocked Whether the token should be soulbound or not. + function setLocked(bool newLocked) external; + + /// Sets the amount of tokens a user can mint from the token. + /// @dev Only callable by the owner. + /// @param newAmount The new amount a user can mint from the token. + function setMintableAmountPerUser(uint256 newAmount) external; + /// @notice Updates the cid for tokenURI. /// @dev Only callable by the owner. /// @param newCid The new cid that points to the updated image. @@ -56,6 +66,10 @@ interface IConfigurableGuildRewardNFT { /// @notice Event emitted whenever the cid is updated. event MetadataUpdate(); + /// Event emitted when the mintableAmountPerUser is changed. + /// @param newAmount The new amount a user can mint from the token. + event MintableAmountPerUserChanged(uint256 newAmount); + /// @notice Error thrown when the token is already claimed. error AlreadyClaimed(); diff --git a/docs/contracts/ConfigurableGuildRewardNFT.md b/docs/contracts/ConfigurableGuildRewardNFT.md index 9cfecf5..c9d7d25 100644 --- a/docs/contracts/ConfigurableGuildRewardNFT.md +++ b/docs/contracts/ConfigurableGuildRewardNFT.md @@ -114,6 +114,42 @@ Burns tokens from the sender. | `userId` | uint256 | The id of the user on Guild. | | `signature` | bytes | The following signed by validSigner: amount, receiver, userId, chainId, the contract's address. | +### setLocked + +```solidity +function setLocked( + bool newLocked +) external +``` + +Sets the locked (i.e. soulboundness) status of all of the tokens in this NFT. + +Only callable by the owner. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `newLocked` | bool | Whether the token should be soulbound or not. | + +### setMintableAmountPerUser + +```solidity +function setMintableAmountPerUser( + uint256 newAmount +) external +``` + +Sets the amount of tokens a user can mint from the token. + +Only callable by the owner. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `newAmount` | uint256 | The new amount a user can mint from the token. | + ### updateTokenURI ```solidity diff --git a/docs/contracts/interfaces/IConfigurableGuildRewardNFT.md b/docs/contracts/interfaces/IConfigurableGuildRewardNFT.md index 2c69dd7..f89d021 100644 --- a/docs/contracts/interfaces/IConfigurableGuildRewardNFT.md +++ b/docs/contracts/interfaces/IConfigurableGuildRewardNFT.md @@ -119,6 +119,42 @@ Burns tokens from the sender. | `userId` | uint256 | The id of the user on Guild. | | `signature` | bytes | The following signed by validSigner: amount, receiver, userId, chainId, the contract's address. | +### setLocked + +```solidity +function setLocked( + bool newLocked +) external +``` + +Sets the locked (i.e. soulboundness) status of all of the tokens in this NFT. + +Only callable by the owner. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `newLocked` | bool | Whether the token should be soulbound or not. | + +### setMintableAmountPerUser + +```solidity +function setMintableAmountPerUser( + uint256 newAmount +) external +``` + +Sets the amount of tokens a user can mint from the token. + +Only callable by the owner. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `newAmount` | uint256 | The new amount a user can mint from the token. | + ### updateTokenURI ```solidity @@ -165,6 +201,22 @@ event MetadataUpdate( Event emitted whenever the cid is updated. +### MintableAmountPerUserChanged + +```solidity +event MintableAmountPerUserChanged( + uint256 newAmount +) +``` + +Event emitted when the mintableAmountPerUser is changed. + +#### Parameters + +| Name | Type | Description | +| :--- | :--- | :---------- | +| `newAmount` | uint256 | The new amount a user can mint from the token. | + ## Custom errors ### AlreadyClaimed From 83b5ebc8398a25630a6c4e9ba36ac01fdc391c33 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Mon, 22 Apr 2024 21:53:33 +0200 Subject: [PATCH 07/18] Add deploy scripts for the configurable NFT --- scripts/deploy-configurable-nft-zksync.ts | 43 +++++++++++++++++++++++ scripts/deploy-configurable-nft.ts | 37 +++++++++++++++++++ scripts/deploy-factory-zksync.ts | 2 +- scripts/deploy-factory.ts | 2 +- 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 scripts/deploy-configurable-nft-zksync.ts create mode 100644 scripts/deploy-configurable-nft.ts diff --git a/scripts/deploy-configurable-nft-zksync.ts b/scripts/deploy-configurable-nft-zksync.ts new file mode 100644 index 0000000..4a5252c --- /dev/null +++ b/scripts/deploy-configurable-nft-zksync.ts @@ -0,0 +1,43 @@ +import { Deployer } from "@matterlabs/hardhat-zksync-deploy"; +import "dotenv/config"; +import * as hre from "hardhat"; +import { Wallet } from "zksync-ethers"; + +// CONFIG +const factoryAddress = "0x..."; /// The address of the proxy to be used when interacting with the factory. +// Note: the values below are just some defaults. The values in clones will truly matter. +const nftConfig = { + name: "", // The name of the token. + symbol: "", // The short, usually all caps symbol of the token. + cid: "", // The cid that will be returned by the tokenURI. + tokenOwner: "0x...", /// The address that will be the owner of the deployed token. + 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. + mintableAmountPerUser: 1 // The maximum amount a user will be able to mint from the token. +}; + +async function main() { + const contractName = "ConfigurableGuildRewardNFT"; + + const zkWallet = new Wallet(process.env.PRIVATE_KEY!); + + const deployer = new Deployer(hre, zkWallet); + + const contract = await deployer.loadArtifact(contractName); + const configurableGuildRewardNFT = await deployer.deploy(contract); + + console.log(`Deploying ${contractName} to zkSync...`); + console.log(`Tx hash: ${configurableGuildRewardNFT.deploymentTransaction()?.hash}`); + + await configurableGuildRewardNFT.waitForDeployment(); + + await configurableGuildRewardNFT.initialize(nftConfig, factoryAddress); + + console.log(`${contractName} deployed to:`, await configurableGuildRewardNFT.getAddress()); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/deploy-configurable-nft.ts b/scripts/deploy-configurable-nft.ts new file mode 100644 index 0000000..0bed985 --- /dev/null +++ b/scripts/deploy-configurable-nft.ts @@ -0,0 +1,37 @@ +import { ethers } from "hardhat"; + +// CONFIG +const factoryAddress = "0x..."; /// The address of the proxy to be used when interacting with the factory. +// Note: the values below are just some defaults. The values in clones will truly matter. +const nftConfig = { + name: "", // The name of the token. + symbol: "", // The short, usually all caps symbol of the token. + cid: "", // The cid that will be returned by the tokenURI. + tokenOwner: "0x...", /// The address that will be the owner of the deployed token. + 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. + mintableAmountPerUser: 1 // The maximum amount a user will be able to mint from the token. +}; + +async function main() { + const contractName = "ConfigurableGuildRewardNFT"; + + const ConfigurableGuildRewardNFT = await ethers.getContractFactory(contractName); + const configurableGuildRewardNFT = await ConfigurableGuildRewardNFT.deploy(); + + const network = await ethers.provider.getNetwork(); + console.log(`Deploying ${contractName} to ${network.name !== "unknown" ? network.name : network.chainId}...`); + console.log(`Tx hash: ${configurableGuildRewardNFT.deploymentTransaction()?.hash}`); + + await configurableGuildRewardNFT.waitForDeployment(); + + await configurableGuildRewardNFT.initialize(nftConfig, factoryAddress); + + console.log(`${contractName} deployed to:`, await configurableGuildRewardNFT.getAddress()); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/deploy-factory-zksync.ts b/scripts/deploy-factory-zksync.ts index 12c1daa..e8158ec 100644 --- a/scripts/deploy-factory-zksync.ts +++ b/scripts/deploy-factory-zksync.ts @@ -7,7 +7,7 @@ import { Wallet } from "zksync-ethers"; const treasury = "0x..."; // The address where the collected fees will go. const fee = 0; // The Guild base fee for every deployment. const validSigner = "0x..."; // The address that signs the parameters for claiming tokens. -// Note: set NFT implementation after deploying the NFT itself (deploy-nft.ts). +// Note: set NFT implementation after deploying the NFTs. async function main() { const contractName = "GuildRewardNFTFactory"; diff --git a/scripts/deploy-factory.ts b/scripts/deploy-factory.ts index 8681229..83ee0cd 100644 --- a/scripts/deploy-factory.ts +++ b/scripts/deploy-factory.ts @@ -4,7 +4,7 @@ import { ethers, upgrades } from "hardhat"; const treasury = "0x..."; // The address where the collected fees will go. const fee = 0; // The Guild base fee for every deployment. const validSigner = "0x..."; // The address that signs the parameters for claiming tokens. -// Note: set NFT implementation after deploying the NFT itself (deploy-nft.ts). +// Note: set NFT implementation after deploying the NFTs. async function main() { const contractName = "GuildRewardNFTFactory"; From 3cbc08a4031ac7c4f2c4a6bc8f91ff7bddbe64c3 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Mon, 22 Apr 2024 21:56:09 +0200 Subject: [PATCH 08/18] Rename deploy-nft -> deploy-basic-nft --- scripts/{deploy-nft-zksync.ts => deploy-basic-nft-zksync.ts} | 0 scripts/{deploy-nft.ts => deploy-basic-nft.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename scripts/{deploy-nft-zksync.ts => deploy-basic-nft-zksync.ts} (100%) rename scripts/{deploy-nft.ts => deploy-basic-nft.ts} (100%) diff --git a/scripts/deploy-nft-zksync.ts b/scripts/deploy-basic-nft-zksync.ts similarity index 100% rename from scripts/deploy-nft-zksync.ts rename to scripts/deploy-basic-nft-zksync.ts diff --git a/scripts/deploy-nft.ts b/scripts/deploy-basic-nft.ts similarity index 100% rename from scripts/deploy-nft.ts rename to scripts/deploy-basic-nft.ts From 25a2c6cd68b7def9538d0af72244232b2281ddc6 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Mon, 22 Apr 2024 22:36:12 +0200 Subject: [PATCH 09/18] Test creating configurable nfts --- test/GuildRewardNFTFactory.spec.ts | 32 ++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/test/GuildRewardNFTFactory.spec.ts b/test/GuildRewardNFTFactory.spec.ts index 3821712..56b89dc 100644 --- a/test/GuildRewardNFTFactory.spec.ts +++ b/test/GuildRewardNFTFactory.spec.ts @@ -9,6 +9,8 @@ const sampleName = "Test Guild Passport"; const sampleSymbol = "TGP"; const cids = ["QmPaZD7i8TpLEeGjHtGoXe4mPKbRNNt8YTHH5nrKoqz9wJ", "QmcaGypWsmzaSQQGuExUjtyTRvZ2FF525Ww6PBNWWgkkLj"]; const sampleFee = 69; +const sampleSoulbound = true; +const sampleMintableAmountPerUser = 1; // CONTRACTS let GuildRewardNFTFactory: ContractFactory; @@ -21,7 +23,8 @@ let treasury: SignerWithAddress; let signer: SignerWithAddress; enum ContractType { - BASIC_NFT + BASIC_NFT, + CONFIGURABLE_NFT } describe("GuildRewardNFTFactory", () => { @@ -48,7 +51,7 @@ describe("GuildRewardNFTFactory", () => { expect(await upgraded.owner()).to.eq(wallet0.address); }); - it("should deploy and initialize clones", async () => { + it("should deploy and initialize clones of BasicGuildRewardNFT", async () => { const basicGuildRewardNFT = await ethers.getContractFactory("BasicGuildRewardNFT"); const nftMain = (await basicGuildRewardNFT.deploy()) as Contract; await nftMain.waitForDeployment(); @@ -63,6 +66,31 @@ describe("GuildRewardNFTFactory", () => { expect(await nft.fee()).to.eq(sampleFee); }); + it("should deploy and initialize clones of ConfigurableGuildRewardNFT", async () => { + const configurableGuildRewardNFT = await ethers.getContractFactory("ConfigurableGuildRewardNFT"); + const nftMain = (await configurableGuildRewardNFT.deploy()) as Contract; + await nftMain.waitForDeployment(); + await factory.setNFTImplementation(ContractType.CONFIGURABLE_NFT, nftMain); + + await factory.deployConfigurableNFT({ + name: sampleName, + symbol: sampleSymbol, + cid: cids[0], + tokenOwner: randomWallet.address, + treasury: treasury.address, + tokenFee: sampleFee, + soulbound: sampleSoulbound, + mintableAmountPerUser: sampleMintableAmountPerUser + }); + const nftAddresses = await factory.getDeployedTokenContracts(wallet0.address); + const nft = nftMain.attach(nftAddresses[0].contractAddress) as Contract; + expect(await nft.name()).to.eq(sampleName); + 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.mintableAmountPerUser()).to.eq(sampleMintableAmountPerUser); + }); + it("should emit RewardNFTDeployed event", async () => { await expect(factory.deployBasicNFT(sampleName, sampleSymbol, cids[0], wallet0.address, treasury.address, 0)) .to.emit(factory, "RewardNFTDeployed") From 358c7e38c0dcb5e56813a2171f80b131f6c71f8d Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Tue, 23 Apr 2024 22:42:52 +0200 Subject: [PATCH 10/18] Fix check for msg.value for multiple mints --- contracts/ConfigurableGuildRewardNFT.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/ConfigurableGuildRewardNFT.sol b/contracts/ConfigurableGuildRewardNFT.sol index 7b26093..b6152cb 100644 --- a/contracts/ConfigurableGuildRewardNFT.sol +++ b/contracts/ConfigurableGuildRewardNFT.sol @@ -69,7 +69,7 @@ contract ConfigurableGuildRewardNFT is } // Fee collection - if (msg.value == guildFee + fee) { + if (msg.value == amount * (guildFee + fee)) { guildTreasury.sendEther(amount * guildFee); treasury.sendEther(amount * fee); } else revert IncorrectFee(msg.value, amount * (guildFee + fee)); From eb7219ab60520662bcf6e255e7020d8d0607c5bc Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Tue, 23 Apr 2024 22:45:20 +0200 Subject: [PATCH 11/18] Add tests for ConfigurableGuildRewardNFT --- test/ConfigurableGuildRewardNFT.spec.ts | 500 ++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 test/ConfigurableGuildRewardNFT.spec.ts diff --git a/test/ConfigurableGuildRewardNFT.spec.ts b/test/ConfigurableGuildRewardNFT.spec.ts new file mode 100644 index 0000000..a865eb4 --- /dev/null +++ b/test/ConfigurableGuildRewardNFT.spec.ts @@ -0,0 +1,500 @@ +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { expect } from "chai"; +import { BigNumberish, Contract, ContractFactory } from "ethers"; +import { ethers, upgrades } from "hardhat"; + +// NFT CONFIG (without owner & treasury) +const sampleCids = ["QmPaZD7i8TpLEeGjHtGoXe4mPKbRNNt8YTHH5nrKoqz9wJ", "QmcaGypWsmzaSQQGuExUjtyTRvZ2FF525Ww6PBNWWgkkLj"]; +const baseNFTConfig = { + name: "Guild NFT", + symbol: "GUILDNFT", + cid: sampleCids[0], + tokenFee: ethers.parseEther("0.1"), + soulbound: true, + mintableAmountPerUser: 1n +}; +let nftConfig: typeof baseNFTConfig & { tokenOwner: string; treasury: string }; +const fee = ethers.parseEther("0.15"); +const sampleAmount = 1n; + +// CONTRACTS +let ConfigurableGuildRewardNFT: ContractFactory; +let nft: Contract; +let nftMultipleMints: Contract; +let GuildRewardNFTFactory: ContractFactory; +let factory: Contract; + +// Test accounts +let wallet0: SignerWithAddress; +let randomWallet: SignerWithAddress; +let treasury: SignerWithAddress; +let adminTreasury: SignerWithAddress; +let signer: SignerWithAddress; + +let chainId: BigNumberish; +const sampleUserId = 42; + +enum ContractType { + BASIC_NFT, + CONFIGURABLE_NFT +} + +const createSignature = async ( + wallet: SignerWithAddress, + amount: BigNumberish, + receiver: string, + userId: number, + chainid: BigNumberish, + nftAddress: string +) => { + const payload = ethers.AbiCoder.defaultAbiCoder().encode( + ["uint256", "address", "uint256", "uint256", "address"], + [amount, receiver, userId, chainid, nftAddress] + ); + const payloadHash = ethers.keccak256(payload); + return wallet.signMessage(ethers.getBytes(payloadHash)); +}; + +describe("ConfigurableGuildRewardNFT", () => { + before("get accounts, setup variables", async () => { + [wallet0, randomWallet, treasury, adminTreasury, signer] = await ethers.getSigners(); + + nftConfig = { + ...baseNFTConfig, + tokenOwner: wallet0.address, + treasury: adminTreasury.address + }; + + chainId = (await ethers.provider.getNetwork()).chainId; + }); + + beforeEach("deploy contract", async () => { + GuildRewardNFTFactory = await ethers.getContractFactory("GuildRewardNFTFactory"); + factory = await upgrades.deployProxy(GuildRewardNFTFactory, [treasury.address, fee, signer.address], { + kind: "uups" + }); + await factory.waitForDeployment(); + + ConfigurableGuildRewardNFT = await ethers.getContractFactory("ConfigurableGuildRewardNFT"); + nft = (await ConfigurableGuildRewardNFT.deploy()) as Contract; + await nft.waitForDeployment(); + await nft.initialize(nftConfig, await factory.getAddress()); + + nftMultipleMints = (await ConfigurableGuildRewardNFT.deploy()) as Contract; + await nftMultipleMints.waitForDeployment(); + await nftMultipleMints.initialize({ ...nftConfig, mintableAmountPerUser: 5 }, await factory.getAddress()); + + await factory.setNFTImplementation(ContractType.CONFIGURABLE_NFT, nft); + await factory.setFee(fee); + }); + + it("should have initialized the state variables", async () => { + 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.mintableAmountPerUser()).to.eq(nftConfig.mintableAmountPerUser); + expect(await nft.factoryProxy()).to.eq(await factory.getAddress()); + }); + + context("Claiming and burning", () => { + let sampleSignature: string; + + beforeEach("create signature", async () => { + sampleSignature = await createSignature( + signer, + sampleAmount, + wallet0.address, + sampleUserId, + chainId, + await nft.getAddress() + ); + }); + + context("#claim", () => { + it("should revert if the address has already claimed all of their tokens", async () => { + await nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { + value: fee + nftConfig.tokenFee + }); + await expect( + nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { + value: fee + nftConfig.tokenFee + }) + ).to.be.revertedWithCustomError(nft, "AlreadyClaimed"); + }); + + it("should revert if the userId has already claimed all of their tokens", async () => { + await nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { + value: fee + nftConfig.tokenFee + }); + await expect( + nft.claim(sampleAmount, randomWallet.address, sampleUserId, sampleSignature, { + value: fee + nftConfig.tokenFee + }) + ).to.be.revertedWithCustomError(nft, "AlreadyClaimed"); + }); + + it("should revert if the signature is incorrect", async () => { + await expect( + nft.claim(sampleAmount, wallet0.address, sampleUserId, ethers.ZeroHash, { + value: fee + nftConfig.tokenFee + }) + ).to.be.revertedWithCustomError(nft, "IncorrectSignature"); + + await expect( + nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature.slice(0, -2), { + value: fee + nftConfig.tokenFee + }) + ).to.be.revertedWithCustomError(nft, "IncorrectSignature"); + + await expect( + nft.claim( + sampleAmount, + wallet0.address, + sampleUserId, + await createSignature( + signer, + sampleAmount, + randomWallet.address, + sampleUserId, + chainId, + await nft.getAddress() + ), + { + value: fee + nftConfig.tokenFee + } + ) + ).to.be.revertedWithCustomError(nft, "IncorrectSignature"); + }); + + it("should increment the total supply", async () => { + const totalSupply0 = await nft.totalSupply(); + await nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { + value: fee + nftConfig.tokenFee + }); + const totalSupply1 = await nft.totalSupply(); + expect(totalSupply1).to.eq(totalSupply0 + 1n); + }); + + it("should increment the address' claimed tokens", async () => { + const userBalance0 = await nft["balanceOf(address)"](wallet0.address); + await nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { + value: fee + nftConfig.tokenFee + }); + const userBalance1 = await nft["balanceOf(address)"](wallet0.address); + expect(userBalance1).to.eq(userBalance0 + sampleAmount); + }); + + it("should increment the userId's claimed tokens", async () => { + const userBalance0 = await nft["balanceOf(uint256)"](sampleUserId); + await nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { + value: fee + nftConfig.tokenFee + }); + const userBalance1 = await nft["balanceOf(uint256)"](sampleUserId); + expect(userBalance1).to.eq(userBalance0 + sampleAmount); + }); + + it("should mint the token", async () => { + const totalSupply = await nft.totalSupply(); + const tokenId = totalSupply; + expect(await nft["balanceOf(address)"](wallet0.address)).to.eq(0); + await expect(nft.ownerOf(tokenId)).to.be.revertedWith("ERC721: invalid token ID"); + await nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { + value: fee + nftConfig.tokenFee + }); + expect(await nft["balanceOf(address)"](wallet0.address)).to.eq(1); + expect(await nft.ownerOf(tokenId)).to.eq(wallet0.address); + }); + + it("should mint multiple tokens", async () => { + const amount = 3n; + const signature = createSignature( + signer, + amount, + wallet0.address, + sampleUserId, + chainId, + await nftMultipleMints.getAddress() + ); + + await factory.setNFTImplementation(ContractType.CONFIGURABLE_NFT, nftMultipleMints); + + const totalSupply = await nftMultipleMints.totalSupply(); + const tokenId = totalSupply; + expect(await nftMultipleMints["balanceOf(address)"](wallet0.address)).to.eq(0); + await expect(nftMultipleMints.ownerOf(tokenId)).to.be.revertedWith("ERC721: invalid token ID"); + await nftMultipleMints.claim(amount, wallet0.address, sampleUserId, signature, { + value: (fee + nftConfig.tokenFee) * amount + }); + expect(await nftMultipleMints["balanceOf(address)"](wallet0.address)).to.eq(3); + expect(await nftMultipleMints.ownerOf(tokenId)).to.eq(wallet0.address); + }); + + it("should emit Locked event when minting", async () => { + const tokenId = await nft.totalSupply(); + await expect( + nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { value: fee + nftConfig.tokenFee }) + ) + .to.emit(nft, "Locked") + .withArgs(tokenId); + }); + + it("should emit Claimed event", async () => { + await expect( + nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { + value: fee + nftConfig.tokenFee + }) + ) + .to.emit(nft, "Claimed") + .withArgs(wallet0.address, 0); + }); + + it("should transfer ether to both treasuries", async () => { + await expect( + nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { + value: fee + nftConfig.tokenFee + }) + ).to.changeEtherBalances( + [wallet0, treasury, adminTreasury], + [-(fee + nftConfig.tokenFee), fee, nftConfig.tokenFee] + ); + }); + + it("should revert if an incorrect msg.value is received", async () => { + await expect( + nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { + value: fee * 2n + }) + ) + .to.be.revertedWithCustomError(nft, "IncorrectFee") + .withArgs(fee * 2n, fee + nftConfig.tokenFee); + + await expect(nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature)) + .to.be.revertedWithCustomError(nft, "IncorrectFee") + .withArgs(0, fee + nftConfig.tokenFee); + }); + }); + + context("#burn", () => { + beforeEach("claim a token", async () => { + await nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { + value: fee + nftConfig.tokenFee + }); + }); + + it("should revert if a token is attempted to be burned by anyone but it's owner", async () => { + await expect( + (nft.connect(randomWallet) as Contract).burn( + [0], + sampleUserId, + createSignature(signer, 1, randomWallet.address, sampleUserId, chainId, await nft.getAddress()) + ) + ).to.be.revertedWithCustomError(nft, "IncorrectSender"); + }); + + it("should revert if the signature is incorrect", async () => { + await expect(nft.burn([0], sampleUserId, ethers.ZeroHash)).to.be.revertedWithCustomError( + nft, + "IncorrectSignature" + ); + + await expect(nft.burn([0], sampleUserId, sampleSignature.slice(0, -2))).to.be.revertedWithCustomError( + nft, + "IncorrectSignature" + ); + + await expect( + nft.burn( + [0], + sampleUserId, + await createSignature( + signer, + sampleAmount, + randomWallet.address, + sampleUserId, + chainId, + await nft.getAddress() + ) + ) + ).to.be.revertedWithCustomError(nft, "IncorrectSignature"); + }); + + it("should decrement the address' claimed tokens", async () => { + const userBalance0 = await nft["balanceOf(address)"](wallet0.address); + await nft.burn([0], sampleUserId, sampleSignature); + const userBalance1 = await nft["balanceOf(address)"](wallet0.address); + expect(userBalance1).to.eq(userBalance0 - 1n); + }); + + it("should decrement the userId's claimed tokens", async () => { + const userBalance0 = await nft["balanceOf(uint256)"](sampleUserId); + await nft.burn([0], sampleUserId, sampleSignature); + const userBalance1 = await nft["balanceOf(uint256)"](sampleUserId); + expect(userBalance1).to.eq(userBalance0 - 1n); + }); + + it("should decrement the total supply", async () => { + const totalSupply0 = await nft.totalSupply(); + await nft.burn([0], sampleUserId, sampleSignature); + const totalSupply1 = await nft.totalSupply(); + expect(totalSupply1).to.eq(totalSupply0 - 1n); + }); + + it("should burn the token", async () => { + const tokenId = 0; + const tokenOfOwnerByIndex = await nft.tokenOfOwnerByIndex(wallet0.address, 0); + expect(tokenOfOwnerByIndex).to.eq(tokenId); + + await nft.burn([tokenId], sampleUserId, sampleSignature); + + await expect(nft.tokenOfOwnerByIndex(wallet0.address, 0)).to.be.revertedWith( + "ERC721Enumerable: owner index out of bounds" + ); + }); + + it("should burn multiple tokens", async () => { + const signature = createSignature( + signer, + 2n, + wallet0.address, + sampleUserId, + chainId, + await nftMultipleMints.getAddress() + ); + + await factory.setNFTImplementation(ContractType.CONFIGURABLE_NFT, nftMultipleMints); + + await nftMultipleMints.claim(2n, wallet0.address, sampleUserId, signature, { + value: (fee + nftConfig.tokenFee) * 2n + }); + + const tokenIds = [0, 1]; + const tokenOfOwnerByIndex0 = await nftMultipleMints.tokenOfOwnerByIndex(wallet0.address, 0); + const tokenOfOwnerByIndex1 = await nftMultipleMints.tokenOfOwnerByIndex(wallet0.address, 1); + expect(tokenOfOwnerByIndex0).to.eq(tokenIds[0]); + expect(tokenOfOwnerByIndex1).to.eq(tokenIds[1]); + + await nftMultipleMints.burn(tokenIds, sampleUserId, signature); + + await expect(nftMultipleMints.tokenOfOwnerByIndex(wallet0.address, 0)).to.be.revertedWith( + "ERC721Enumerable: owner index out of bounds" + ); + await expect(nftMultipleMints.tokenOfOwnerByIndex(wallet0.address, 1)).to.be.revertedWith( + "ERC721Enumerable: owner index out of bounds" + ); + }); + }); + }); + + context("General config", () => { + context("#setLocked", () => { + beforeEach("claim a token", async () => { + await nft.claim( + sampleAmount, + wallet0.address, + sampleUserId, + await createSignature(signer, sampleAmount, wallet0.address, sampleUserId, chainId, await nft.getAddress()), + { + value: fee + nftConfig.tokenFee + } + ); + }); + + it("should revert if the lock status is attempted to be changed by anyone but the owner", async () => { + await expect((nft.connect(randomWallet) as Contract).setLocked(false)).to.be.revertedWith( + "Ownable: caller is not the owner" + ); + }); + + it("should update locked status", async () => { + const locked = false; + await nft.setLocked(locked); + const newLocked = await nft.locked(0); + expect(newLocked).to.eq(locked); + }); + + it("should emit Locked/Unlocked event", async () => { + await expect(nft.setLocked(false)).to.emit(nft, "Unlocked").withArgs(0); + await expect(nft.setLocked(true)).to.emit(nft, "Locked").withArgs(0); + }); + }); + + 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( + "Ownable: caller is not the owner" + ); + }); + + it("should update mintableAmountPerUser", async () => { + const mintableAmountPerUser = 5; + await nft.setMintableAmountPerUser(mintableAmountPerUser); + const newMintableAmountPerUser = await nft.mintableAmountPerUser(); + expect(newMintableAmountPerUser).to.eq(mintableAmountPerUser); + }); + + it("should emit MintableAmountPerUserChanged event", async () => { + const mintableAmountPerUser = 5; + await expect(nft.setMintableAmountPerUser(mintableAmountPerUser)) + .to.emit(nft, "MintableAmountPerUserChanged") + .withArgs(mintableAmountPerUser); + }); + }); + }); + + context("TokenURI", () => { + let signature: string; + + beforeEach("create signature, set strings", async () => { + signature = await createSignature( + signer, + sampleAmount, + wallet0.address, + sampleUserId, + chainId, + await nft.getAddress() + ); + }); + + context("#tokenURI", () => { + it("should revert when trying to get the tokenURI for a non-existent token", async () => { + await expect(nft.tokenURI(84)).to.revertedWithCustomError(nft, "NonExistentToken").withArgs(84); + }); + + it("should return the correct tokenURI", async () => { + await nft.claim(sampleAmount, wallet0.address, sampleUserId, signature, { + value: fee + nftConfig.tokenFee + }); + + const tokenURI = await nft.tokenURI(0); + const regex = new RegExp(`ipfs://${nftConfig.cid}`); + expect(regex.test(tokenURI)).to.eq(true); + }); + }); + + context("#updateTokenURI", () => { + beforeEach("claim a token", async () => { + await nft.claim(sampleAmount, wallet0.address, sampleUserId, signature, { + value: fee + nftConfig.tokenFee + }); + }); + + it("should revert if the cid is attempted to be changed by anyone but the owner", async () => { + await expect((nft.connect(randomWallet) as Contract).updateTokenURI(sampleCids[1])).to.be.revertedWith( + "Ownable: caller is not the owner" + ); + }); + + it("should update cid", async () => { + const oldTokenURI = await nft.tokenURI(0); + await nft.updateTokenURI(sampleCids[1]); + const newTokenURI = await nft.tokenURI(0); + expect(newTokenURI).to.not.eq(oldTokenURI); + expect(newTokenURI).to.contain(sampleCids[1]); + }); + + it("should emit MetadataUpdate event", async () => { + await expect(nft.updateTokenURI(sampleCids[1])).to.emit(nft, "MetadataUpdate").withArgs(); + }); + }); + }); +}); From bd5feb880056b8978e72c64c5d4fd579d7e9afba Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Tue, 23 Apr 2024 23:22:29 +0200 Subject: [PATCH 12/18] Remove unnecessary using --- contracts/BasicGuildRewardNFT.sol | 1 - contracts/ConfigurableGuildRewardNFT.sol | 1 - 2 files changed, 2 deletions(-) diff --git a/contracts/BasicGuildRewardNFT.sol b/contracts/BasicGuildRewardNFT.sol index 1445342..cf10c06 100644 --- a/contracts/BasicGuildRewardNFT.sol +++ b/contracts/BasicGuildRewardNFT.sol @@ -20,7 +20,6 @@ contract BasicGuildRewardNFT is TreasuryManager { using ECDSA for bytes32; - using LibTransfer for address; using LibTransfer for address payable; address public factoryProxy; diff --git a/contracts/ConfigurableGuildRewardNFT.sol b/contracts/ConfigurableGuildRewardNFT.sol index b6152cb..f8fcf6d 100644 --- a/contracts/ConfigurableGuildRewardNFT.sol +++ b/contracts/ConfigurableGuildRewardNFT.sol @@ -20,7 +20,6 @@ contract ConfigurableGuildRewardNFT is TreasuryManager { using ECDSA for bytes32; - using LibTransfer for address; using LibTransfer for address payable; address public factoryProxy; From dc1055c40be358f649d88ef854502f51baeef34d Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Wed, 24 Apr 2024 13:48:13 +0200 Subject: [PATCH 13/18] Remove unsafeSkipStorageCheck (leftover from testing) --- scripts/upgrade-factory.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/upgrade-factory.ts b/scripts/upgrade-factory.ts index 0637baa..3799326 100644 --- a/scripts/upgrade-factory.ts +++ b/scripts/upgrade-factory.ts @@ -7,8 +7,7 @@ async function main() { const GuildRewardNFTFactory = await ethers.getContractFactory(contractName); const guildRewardNFTFactory = await upgrades.upgradeProxy(factoryAddress, GuildRewardNFTFactory, { - kind: "uups", - unsafeSkipStorageCheck: true + kind: "uups" // call: { fn: "reInitialize", args: [] } }); From aec82dc9d9bee9c194e2eced44394f3c449ea827 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Wed, 24 Apr 2024 14:47:29 +0200 Subject: [PATCH 14/18] Deploy to Sepolia --- .openzeppelin/sepolia.json | 199 +++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/.openzeppelin/sepolia.json b/.openzeppelin/sepolia.json index 9b7a1a2..669ab28 100644 --- a/.openzeppelin/sepolia.json +++ b/.openzeppelin/sepolia.json @@ -205,6 +205,205 @@ }, "namespaces": {} } + }, + "84edc5560991480cc63dffc81724b4488934843b6a74795c47dbcbdcbf9ef004": { + "address": "0x3aBded0A50f4Ad3c2f32CF58Ebaebf7b60EAD2B6", + "txHash": "0xeeb6a6b011dd6876e0272c271bbd2e2c45667b57aaa8fe4f50f34727ac148bf4", + "layout": { + "solcVersion": "0.8.19", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "_owner", + "offset": 0, + "slot": "151", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "treasury", + "offset": 0, + "slot": "201", + "type": "t_address_payable", + "contract": "TreasuryManager", + "src": "contracts/utils/TreasuryManager.sol:10" + }, + { + "label": "fee", + "offset": 0, + "slot": "202", + "type": "t_uint256", + "contract": "TreasuryManager", + "src": "contracts/utils/TreasuryManager.sol:12" + }, + { + "label": "__gap", + "offset": 0, + "slot": "203", + "type": "t_array(t_uint256)48_storage", + "contract": "TreasuryManager", + "src": "contracts/utils/TreasuryManager.sol:15" + }, + { + "label": "validSigner", + "offset": 0, + "slot": "251", + "type": "t_address", + "contract": "GuildRewardNFTFactory", + "src": "contracts/GuildRewardNFTFactory.sol:21" + }, + { + "label": "nftImplementations", + "offset": 0, + "slot": "252", + "type": "t_mapping(t_enum(ContractType)7072,t_address)", + "contract": "GuildRewardNFTFactory", + "src": "contracts/GuildRewardNFTFactory.sol:23" + }, + { + "label": "deployedTokenContracts", + "offset": 0, + "slot": "253", + "type": "t_mapping(t_address,t_array(t_struct(Deployment)7095_storage)dyn_storage)", + "contract": "GuildRewardNFTFactory", + "src": "contracts/GuildRewardNFTFactory.sol:24" + }, + { + "label": "__gap", + "offset": 0, + "slot": "254", + "type": "t_array(t_uint256)47_storage", + "contract": "GuildRewardNFTFactory", + "src": "contracts/GuildRewardNFTFactory.sol:27" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_address_payable": { + "label": "address payable", + "numberOfBytes": "20" + }, + "t_array(t_struct(Deployment)7095_storage)dyn_storage": { + "label": "struct IGuildRewardNFTFactory.Deployment[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)47_storage": { + "label": "uint256[47]", + "numberOfBytes": "1504" + }, + "t_array(t_uint256)48_storage": { + "label": "uint256[48]", + "numberOfBytes": "1536" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_enum(ContractType)7072": { + "label": "enum IGuildRewardNFTFactory.ContractType", + "members": [ + "BASIC_NFT", + "CONFIGURABLE_NFT" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_array(t_struct(Deployment)7095_storage)dyn_storage)": { + "label": "mapping(address => struct IGuildRewardNFTFactory.Deployment[])", + "numberOfBytes": "32" + }, + "t_mapping(t_enum(ContractType)7072,t_address)": { + "label": "mapping(enum IGuildRewardNFTFactory.ContractType => address)", + "numberOfBytes": "32" + }, + "t_struct(Deployment)7095_storage": { + "label": "struct IGuildRewardNFTFactory.Deployment", + "members": [ + { + "label": "contractAddress", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "contractType", + "type": "t_enum(ContractType)7072", + "offset": 20, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } } } } From 26bc7a2f4e9728f434e90271caebba53351e8bca Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Fri, 26 Apr 2024 00:05:37 +0200 Subject: [PATCH 15/18] Optimize ConfigurableGuildRewardNFT --- contracts/ConfigurableGuildRewardNFT.sol | 33 +++++++++++++++--------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/contracts/ConfigurableGuildRewardNFT.sol b/contracts/ConfigurableGuildRewardNFT.sol index f8fcf6d..23e3825 100644 --- a/contracts/ConfigurableGuildRewardNFT.sol +++ b/contracts/ConfigurableGuildRewardNFT.sol @@ -46,10 +46,9 @@ contract ConfigurableGuildRewardNFT is } function claim(uint256 amount, address receiver, uint256 userId, bytes calldata signature) external payable { - if ( - amount > mintableAmountPerUser - balanceOf(receiver) || - amount > mintableAmountPerUser - claimedTokens[userId] - ) revert AlreadyClaimed(); + uint256 mintableAmount = mintableAmountPerUser; + if (amount > mintableAmount - balanceOf(receiver) || amount > mintableAmount - claimedTokens[userId]) + revert AlreadyClaimed(); if (!isValidSignature(amount, receiver, userId, signature)) revert IncorrectSignature(); (uint256 guildFee, address payable guildTreasury) = ITreasuryManager(factoryProxy).getFeeData(); @@ -59,32 +58,42 @@ contract ConfigurableGuildRewardNFT is uint256 firstTokenId = totalSupply(); uint256 lastTokenId = firstTokenId + amount - 1; - for (uint256 tokenId = firstTokenId; tokenId <= lastTokenId; ++tokenId) { + for (uint256 tokenId = firstTokenId; tokenId <= lastTokenId; ) { _safeMint(receiver, tokenId); if (soulbound) emit Locked(tokenId); emit Claimed(receiver, tokenId); + + unchecked { + ++tokenId; + } } // Fee collection - if (msg.value == amount * (guildFee + fee)) { - guildTreasury.sendEther(amount * guildFee); - treasury.sendEther(amount * fee); - } else revert IncorrectFee(msg.value, amount * (guildFee + fee)); + uint256 guildAmount = amount * guildFee; + uint256 ownerAmount = amount * fee; + if (msg.value == guildAmount + ownerAmount) { + guildTreasury.sendEther(guildAmount); + treasury.sendEther(ownerAmount); + } else revert IncorrectFee(msg.value, guildAmount + ownerAmount); } function burn(uint256[] calldata tokenIds, uint256 userId, bytes calldata signature) external { uint256 amount = tokenIds.length; if (!isValidSignature(amount, msg.sender, userId, signature)) revert IncorrectSignature(); - for (uint256 i; i < amount; ++i) { + for (uint256 i; i < amount; ) { uint256 tokenId = tokenIds[i]; - if (msg.sender != ownerOf(tokenId)) revert IncorrectSender(); - claimedTokens[userId]--; _burn(tokenId); + + unchecked { + ++i; + } } + + claimedTokens[userId] -= amount; } function setLocked(bool newLocked) external onlyOwner { From 9cbd8750de6be2272f6a42b1ca84c27d763b1781 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Fri, 26 Apr 2024 00:07:15 +0200 Subject: [PATCH 16/18] Use a modifier for repeated code in OptionallySoulboundERC721 --- contracts/token/OptionallySoulboundERC721.sol | 27 ++++++++++--------- .../token/OptionallySoulboundERC721.md | 10 +++++++ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/contracts/token/OptionallySoulboundERC721.sol b/contracts/token/OptionallySoulboundERC721.sol index 8eb15d4..52cf391 100644 --- a/contracts/token/OptionallySoulboundERC721.sol +++ b/contracts/token/OptionallySoulboundERC721.sol @@ -26,6 +26,12 @@ contract OptionallySoulboundERC721 is ERC721Upgradeable, ERC721EnumerableUpgrade /// @notice Error thrown when a function's execution is not possible, because the soulbound mode is on. error Soulbound(); + /// @notice Reverts the function execution if the token is soulbound. + modifier checkSoulbound() { + if (soulbound) revert Soulbound(); + _; + } + // solhint-disable-next-line func-name-mixedcase function __OptionallySoulboundERC721_init( string memory name_, @@ -52,24 +58,24 @@ contract OptionallySoulboundERC721 is ERC721Upgradeable, ERC721EnumerableUpgrade return soulbound; } - function approve(address to, uint256 tokenId) public virtual override(IERC721Upgradeable, ERC721Upgradeable) { - if (soulbound) revert Soulbound(); + function approve( + address to, + uint256 tokenId + ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) checkSoulbound { super.approve(to, tokenId); } function setApprovalForAll( address operator, bool approved - ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) { - if (soulbound) revert Soulbound(); + ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) checkSoulbound { super.setApprovalForAll(operator, approved); } function isApprovedForAll( address owner, address operator - ) public view virtual override(IERC721Upgradeable, ERC721Upgradeable) returns (bool) { - if (soulbound) revert Soulbound(); + ) public view virtual override(IERC721Upgradeable, ERC721Upgradeable) checkSoulbound returns (bool) { return super.isApprovedForAll(owner, operator); } @@ -77,8 +83,7 @@ contract OptionallySoulboundERC721 is ERC721Upgradeable, ERC721EnumerableUpgrade address from, address to, uint256 tokenId - ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) { - if (soulbound) revert Soulbound(); + ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) checkSoulbound { super.transferFrom(from, to, tokenId); } @@ -86,8 +91,7 @@ contract OptionallySoulboundERC721 is ERC721Upgradeable, ERC721EnumerableUpgrade address from, address to, uint256 tokenId - ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) { - if (soulbound) revert Soulbound(); + ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) checkSoulbound { super.safeTransferFrom(from, to, tokenId); } @@ -96,8 +100,7 @@ contract OptionallySoulboundERC721 is ERC721Upgradeable, ERC721EnumerableUpgrade address to, uint256 tokenId, bytes memory data - ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) { - if (soulbound) revert Soulbound(); + ) public virtual override(IERC721Upgradeable, ERC721Upgradeable) checkSoulbound { super.safeTransferFrom(from, to, tokenId, data); } diff --git a/docs/contracts/token/OptionallySoulboundERC721.md b/docs/contracts/token/OptionallySoulboundERC721.md index 5dbe59d..84feffa 100644 --- a/docs/contracts/token/OptionallySoulboundERC721.md +++ b/docs/contracts/token/OptionallySoulboundERC721.md @@ -198,6 +198,16 @@ Used for minting/burning even when soulbound. | `firstTokenId` | uint256 | | | `batchSize` | uint256 | | +## Modifiers + +### checkSoulbound + +```solidity +modifier checkSoulbound() +``` + +Reverts the function execution if the token is soulbound. + ## Custom errors ### NonExistentToken From 6ee6bb757d445d521c0b98edad042432b90732f0 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Fri, 26 Apr 2024 00:08:02 +0200 Subject: [PATCH 17/18] Add Unlocked event when minting non-soulbound tokens --- contracts/ConfigurableGuildRewardNFT.sol | 1 + test/ConfigurableGuildRewardNFT.spec.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/ConfigurableGuildRewardNFT.sol b/contracts/ConfigurableGuildRewardNFT.sol index 23e3825..072989d 100644 --- a/contracts/ConfigurableGuildRewardNFT.sol +++ b/contracts/ConfigurableGuildRewardNFT.sol @@ -62,6 +62,7 @@ contract ConfigurableGuildRewardNFT is _safeMint(receiver, tokenId); if (soulbound) emit Locked(tokenId); + else emit Unlocked(tokenId); emit Claimed(receiver, tokenId); diff --git a/test/ConfigurableGuildRewardNFT.spec.ts b/test/ConfigurableGuildRewardNFT.spec.ts index a865eb4..43be9d0 100644 --- a/test/ConfigurableGuildRewardNFT.spec.ts +++ b/test/ConfigurableGuildRewardNFT.spec.ts @@ -229,7 +229,7 @@ describe("ConfigurableGuildRewardNFT", () => { expect(await nftMultipleMints.ownerOf(tokenId)).to.eq(wallet0.address); }); - it("should emit Locked event when minting", async () => { + it("should emit Locked event when minting soulbound tokens", async () => { const tokenId = await nft.totalSupply(); await expect( nft.claim(sampleAmount, wallet0.address, sampleUserId, sampleSignature, { value: fee + nftConfig.tokenFee }) From 4e77ea461d8003deaad7f56ba0bd1b4519ca66c7 Mon Sep 17 00:00:00 2001 From: Tomi_Ohl Date: Fri, 26 Apr 2024 00:39:18 +0200 Subject: [PATCH 18/18] Fix some comments --- contracts/interfaces/IConfigurableGuildRewardNFT.sol | 6 +++--- contracts/interfaces/IGuildRewardNFTFactory.sol | 4 ++-- docs/contracts/GuildRewardNFTFactory.md | 2 +- docs/contracts/interfaces/IGuildRewardNFTFactory.md | 2 +- scripts/deploy-basic-nft-zksync.ts | 4 ++-- scripts/deploy-basic-nft.ts | 4 ++-- scripts/deploy-configurable-nft-zksync.ts | 4 ++-- scripts/deploy-configurable-nft.ts | 4 ++-- test/GuildRewardNFTFactory.spec.ts | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/contracts/interfaces/IConfigurableGuildRewardNFT.sol b/contracts/interfaces/IConfigurableGuildRewardNFT.sol index 27bc637..b6da4c2 100644 --- a/contracts/interfaces/IConfigurableGuildRewardNFT.sol +++ b/contracts/interfaces/IConfigurableGuildRewardNFT.sol @@ -43,12 +43,12 @@ interface IConfigurableGuildRewardNFT { /// @param signature The following signed by validSigner: amount, receiver, userId, chainId, the contract's address. function burn(uint256[] calldata tokenIds, uint256 userId, bytes calldata signature) external; - /// Sets the locked (i.e. soulboundness) status of all of the tokens in this NFT. + /// @notice Sets the locked (i.e. soulboundness) status of all of the tokens in this NFT. /// @dev Only callable by the owner. /// @param newLocked Whether the token should be soulbound or not. function setLocked(bool newLocked) external; - /// Sets the amount of tokens a user can mint from the token. + /// @notice Sets the amount of tokens a user can mint from the token. /// @dev Only callable by the owner. /// @param newAmount The new amount a user can mint from the token. function setMintableAmountPerUser(uint256 newAmount) external; @@ -66,7 +66,7 @@ interface IConfigurableGuildRewardNFT { /// @notice Event emitted whenever the cid is updated. event MetadataUpdate(); - /// Event emitted when the mintableAmountPerUser is changed. + /// @notice Event emitted when the mintableAmountPerUser is changed. /// @param newAmount The new amount a user can mint from the token. event MintableAmountPerUserChanged(uint256 newAmount); diff --git a/contracts/interfaces/IGuildRewardNFTFactory.sol b/contracts/interfaces/IGuildRewardNFTFactory.sol index 430f5a4..5dc82e2 100644 --- a/contracts/interfaces/IGuildRewardNFTFactory.sol +++ b/contracts/interfaces/IGuildRewardNFTFactory.sol @@ -16,7 +16,7 @@ interface IGuildRewardNFTFactory { /// @param symbol The symbol of the NFT to be created. /// @param cid The cid used to construct the tokenURI of the NFT to be created. /// @param tokenOwner The address that will be the owner of the deployed token. - /// @param tokenTreasury The address that will collect the prices of the minted deployed tokens. + /// @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 mintableAmountPerUser The maximum amount a user will be able to mint from the deployed token. @@ -59,7 +59,7 @@ interface IGuildRewardNFTFactory { /// @param symbol The symbol of the NFT to be created. /// @param cid The cid used to construct the tokenURI of the NFT to be created. /// @param tokenOwner The address that will be the owner of the deployed token. - /// @param tokenTreasury The address that will collect the prices of the minted deployed tokens. + /// @param tokenTreasury The address that will collect the prices of the minted tokens. /// @param tokenFee The price of every mint in wei. function deployBasicNFT( string calldata name, diff --git a/docs/contracts/GuildRewardNFTFactory.md b/docs/contracts/GuildRewardNFTFactory.md index b70df0e..c9803f7 100644 --- a/docs/contracts/GuildRewardNFTFactory.md +++ b/docs/contracts/GuildRewardNFTFactory.md @@ -86,7 +86,7 @@ Deploys a minimal proxy for a basic NFT. | `symbol` | string | The symbol of the NFT to be created. | | `cid` | string | The cid used to construct the tokenURI of the NFT to be created. | | `tokenOwner` | address | The address that will be the owner of the deployed token. | -| `tokenTreasury` | address payable | The address that will collect the prices of the minted deployed tokens. | +| `tokenTreasury` | address payable | The address that will collect the prices of the minted tokens. | | `tokenFee` | uint256 | The price of every mint in wei. | ### deployConfigurableNFT diff --git a/docs/contracts/interfaces/IGuildRewardNFTFactory.md b/docs/contracts/interfaces/IGuildRewardNFTFactory.md index 885b53f..e135687 100644 --- a/docs/contracts/interfaces/IGuildRewardNFTFactory.md +++ b/docs/contracts/interfaces/IGuildRewardNFTFactory.md @@ -81,7 +81,7 @@ Deploys a minimal proxy for a basic NFT. | `symbol` | string | The symbol of the NFT to be created. | | `cid` | string | The cid used to construct the tokenURI of the NFT to be created. | | `tokenOwner` | address | The address that will be the owner of the deployed token. | -| `tokenTreasury` | address payable | The address that will collect the prices of the minted deployed tokens. | +| `tokenTreasury` | address payable | The address that will collect the prices of the minted tokens. | | `tokenFee` | uint256 | The price of every mint in wei. | ### deployConfigurableNFT diff --git a/scripts/deploy-basic-nft-zksync.ts b/scripts/deploy-basic-nft-zksync.ts index 38a888c..8a5bd99 100644 --- a/scripts/deploy-basic-nft-zksync.ts +++ b/scripts/deploy-basic-nft-zksync.ts @@ -4,12 +4,12 @@ import * as hre from "hardhat"; import { Wallet } from "zksync-ethers"; // CONFIG -const factoryAddress = "0x..."; /// The address of the proxy to be used when interacting with the factory. +const factoryAddress = "0x..."; // The address of the proxy to be used when interacting with the factory. // Note: the values below are just some defaults. The values in clones will truly matter. const name = ""; // The name of the token. const symbol = ""; // The short, usually all caps symbol of the token. const cid = ""; // The cid that will be returned by the tokenURI. -const tokenOwner = "0x..."; /// The address that will be the owner of the deployed token. +const tokenOwner = "0x..."; // The address that will be the owner of the deployed token. const treasury = "0x..."; // The address that will receive the price paid for mints. const tokenFee = 0; // The price of every mint in wei. diff --git a/scripts/deploy-basic-nft.ts b/scripts/deploy-basic-nft.ts index 403d708..aab8028 100644 --- a/scripts/deploy-basic-nft.ts +++ b/scripts/deploy-basic-nft.ts @@ -1,12 +1,12 @@ import { ethers } from "hardhat"; // CONFIG -const factoryAddress = "0x..."; /// The address of the proxy to be used when interacting with the factory. +const factoryAddress = "0x..."; // The address of the proxy to be used when interacting with the factory. // Note: the values below are just some defaults. The values in clones will truly matter. const name = ""; // The name of the token. const symbol = ""; // The short, usually all caps symbol of the token. const cid = ""; // The cid that will be returned by the tokenURI. -const tokenOwner = "0x..."; /// The address that will be the owner of the deployed token. +const tokenOwner = "0x..."; // The address that will be the owner of the deployed token. const treasury = "0x..."; // The address that will receive the price paid for mints. const tokenFee = 0; // The price of every mint in wei. diff --git a/scripts/deploy-configurable-nft-zksync.ts b/scripts/deploy-configurable-nft-zksync.ts index 4a5252c..0f1832c 100644 --- a/scripts/deploy-configurable-nft-zksync.ts +++ b/scripts/deploy-configurable-nft-zksync.ts @@ -4,13 +4,13 @@ import * as hre from "hardhat"; import { Wallet } from "zksync-ethers"; // CONFIG -const factoryAddress = "0x..."; /// The address of the proxy to be used when interacting with the factory. +const factoryAddress = "0x..."; // The address of the proxy to be used when interacting with the factory. // Note: the values below are just some defaults. The values in clones will truly matter. const nftConfig = { name: "", // The name of the token. symbol: "", // The short, usually all caps symbol of the token. cid: "", // The cid that will be returned by the tokenURI. - tokenOwner: "0x...", /// The address that will be the owner of the deployed token. + tokenOwner: "0x...", // The address that will be the owner of the deployed token. 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. diff --git a/scripts/deploy-configurable-nft.ts b/scripts/deploy-configurable-nft.ts index 0bed985..57fe13e 100644 --- a/scripts/deploy-configurable-nft.ts +++ b/scripts/deploy-configurable-nft.ts @@ -1,13 +1,13 @@ import { ethers } from "hardhat"; // CONFIG -const factoryAddress = "0x..."; /// The address of the proxy to be used when interacting with the factory. +const factoryAddress = "0x..."; // The address of the proxy to be used when interacting with the factory. // Note: the values below are just some defaults. The values in clones will truly matter. const nftConfig = { name: "", // The name of the token. symbol: "", // The short, usually all caps symbol of the token. cid: "", // The cid that will be returned by the tokenURI. - tokenOwner: "0x...", /// The address that will be the owner of the deployed token. + tokenOwner: "0x...", // The address that will be the owner of the deployed token. 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. diff --git a/test/GuildRewardNFTFactory.spec.ts b/test/GuildRewardNFTFactory.spec.ts index 56b89dc..4b072ea 100644 --- a/test/GuildRewardNFTFactory.spec.ts +++ b/test/GuildRewardNFTFactory.spec.ts @@ -4,7 +4,7 @@ import { expect } from "chai"; import { Contract, ContractFactory } from "ethers"; import { ethers, upgrades } from "hardhat"; -/// NFT CONFIG +// NFT CONFIG const sampleName = "Test Guild Passport"; const sampleSymbol = "TGP"; const cids = ["QmPaZD7i8TpLEeGjHtGoXe4mPKbRNNt8YTHH5nrKoqz9wJ", "QmcaGypWsmzaSQQGuExUjtyTRvZ2FF525Ww6PBNWWgkkLj"];