Skip to content

Commit

Permalink
Configurable NFT (#7)
Browse files Browse the repository at this point in the history
* Add contractType to RewardNFTDeployed event

* Add ConfigurableGuildRewardNFT

* Add function to factory that deploys configurable NFTs

* Unify comments

* Update docs

* Add configuration functions for owner

* Add deploy scripts for the configurable NFT

* Rename deploy-nft -> deploy-basic-nft

* Test creating configurable nfts

* Fix check for msg.value for multiple mints

* Add tests for ConfigurableGuildRewardNFT

* Remove unnecessary using

* Remove unsafeSkipStorageCheck (leftover from testing)

* Deploy to Sepolia

* Optimize ConfigurableGuildRewardNFT

* Use a modifier for repeated code in OptionallySoulboundERC721

* Add Unlocked event when minting non-soulbound tokens

* Fix some comments
  • Loading branch information
TomiOhl authored Apr 25, 2024
1 parent caa4064 commit 7166a8c
Show file tree
Hide file tree
Showing 24 changed files with 1,999 additions and 37 deletions.
199 changes: 199 additions & 0 deletions .openzeppelin/sepolia.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
}
}
}
1 change: 0 additions & 1 deletion contracts/BasicGuildRewardNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ contract BasicGuildRewardNFT is
TreasuryManager
{
using ECDSA for bytes32;
using LibTransfer for address;
using LibTransfer for address payable;

address public factoryProxy;
Expand Down
138 changes: 138 additions & 0 deletions contracts/ConfigurableGuildRewardNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//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 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 {
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();

claimedTokens[userId] += amount;

uint256 firstTokenId = totalSupply();
uint256 lastTokenId = firstTokenId + amount - 1;

for (uint256 tokenId = firstTokenId; tokenId <= lastTokenId; ) {
_safeMint(receiver, tokenId);

if (soulbound) emit Locked(tokenId);
else emit Unlocked(tokenId);

emit Claimed(receiver, tokenId);

unchecked {
++tokenId;
}
}

// Fee collection
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; ) {
uint256 tokenId = tokenIds[i];
if (msg.sender != ownerOf(tokenId)) revert IncorrectSender();
_burn(tokenId);

unchecked {
++i;
}
}

claimedTokens[userId] -= amount;
}

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();
}

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();
}
}
22 changes: 19 additions & 3 deletions contracts/GuildRewardNFTFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -39,16 +40,31 @@ 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 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 {
Expand Down
Loading

0 comments on commit 7166a8c

Please sign in to comment.