Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable NFT #7

Merged
merged 19 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading