From 0f4753dd956dcc10e23a121e206024e2dbd1df1f Mon Sep 17 00:00:00 2001 From: ogarciarevett Date: Thu, 5 Feb 2026 04:28:33 +0100 Subject: [PATCH 1/4] Feat: Add rewards like a semi-diamond proxy with a central storage --- contracts/upgradeables/soulbounds/Rewards.sol | 651 ++++-------------- .../upgradeables/soulbounds/RewardsState.sol | 244 +++++++ .../upgradeables/soulbounds/Treasury.sol | 405 +++++++++++ hardhat.config.ts | 2 +- scripts/upgradeRewards.ts | 96 +++ 5 files changed, 873 insertions(+), 525 deletions(-) create mode 100644 contracts/upgradeables/soulbounds/RewardsState.sol create mode 100644 contracts/upgradeables/soulbounds/Treasury.sol create mode 100644 scripts/upgradeRewards.ts diff --git a/contracts/upgradeables/soulbounds/Rewards.sol b/contracts/upgradeables/soulbounds/Rewards.sol index c620eb7..2d2672b 100644 --- a/contracts/upgradeables/soulbounds/Rewards.sol +++ b/contracts/upgradeables/soulbounds/Rewards.sol @@ -53,6 +53,8 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgrade import { AccessToken } from "../../soulbounds/AccessToken.sol"; import { ERCWhitelistSignatureUpgradeable } from "../ercs/ERCWhitelistSignatureUpgradeable.sol"; import { LibItems } from "../../libraries/LibItems.sol"; +import { Treasury } from "./Treasury.sol"; +import { RewardsState } from "./RewardsState.sol"; contract Rewards is Initializable, @@ -100,33 +102,10 @@ contract Rewards is //////////////////////////////////////////////////////////////*/ AccessToken private rewardTokenContract; - uint256[] public itemIds; - mapping(uint256 => bool) private tokenExists; - mapping(uint256 => LibItems.RewardToken) public tokenRewards; - mapping(uint256 => bool) public isTokenMintPaused; // tokenId => bool - default is false - mapping(uint256 => bool) public isClaimRewardPaused; // tokenId => bool - default is false - mapping(uint256 => mapping(uint256 => uint256)) - private erc721RewardCurrentIndex; // rewardTokenId => rewardIndex => erc721RewardCurrentIndex - mapping(uint256 => uint256) public currentRewardSupply; // rewardTokenId => currentRewardSupply + address public treasury; // Address of Treasury contract for treasury management and queries + address public rewardsState; // Address of RewardsState contract for centralized state management - // Treasury system - mapping(address => bool) public whitelistedTokens; // token address => whitelisted - address[] private whitelistedTokenList; // list of whitelisted token addresses - mapping(address => uint256) public reservedAmounts; // token address => reserved amount - - // Per-user nonce tracking - mapping(address => mapping(uint256 => bool)) public userNonces; // user => nonce => used - - // ERC721 Reservation Tracking - mapping(address => mapping(uint256 => bool)) public isErc721Reserved; // token address => tokenId => isReserved - mapping(address => uint256) public erc721TotalReserved; // token address => reserved amounts - - // ERC1155 Reservation Tracking - mapping(address => mapping(uint256 => uint256)) public erc1155ReservedAmounts; // token address => tokenId => reserved - mapping(address => uint256) public erc1155TotalReserved; // token address => total reserved (all IDs) - mapping(address => LibItems.RewardType) public tokenTypes; // token address => type - - uint256[33] private __gap; + uint256[46] private __gap; /*////////////////////////////////////////////////////////////// EVENTS @@ -197,6 +176,10 @@ contract Rewards is _checkRole(UPGRADER_ROLE); } + function _state() private view returns (RewardsState) { + return RewardsState(rewardsState); + } + function updateRewardTokenContract( address _rewardTokenAddress ) external onlyRole(DEV_CONFIG_ROLE) { @@ -208,224 +191,24 @@ contract Rewards is } function isTokenExist(uint256 _tokenId) public view returns (bool) { - return tokenExists[_tokenId]; + return _state().isTokenExists(_tokenId); } function getRewardTokenContract() external view returns (address) { return address(rewardTokenContract); } - /** - * @dev Get treasury balances for all whitelisted tokens with full balance breakdown. - * Includes ERC20 tokens (fa), ERC721 tokens (nft), and ERC1155 tokens (nft) from the treasury. - * @return addresses Array of token addresses. - * @return totalBalances Array of total balances in the contract. - * @return reservedBalances Array of reserved amounts for rewards. - * @return availableBalances Array of available (unreserved) balances. - * @return symbols Array of token symbols. - * @return names Array of token names. - * @return types Array of token types ("fa" for fungible assets, "nft" for NFTs). - */ - function getAllTreasuryBalances() - external - view - returns ( - address[] memory addresses, - uint256[] memory totalBalances, - uint256[] memory reservedBalances, - uint256[] memory availableBalances, - string[] memory symbols, - string[] memory names, - string[] memory types - ) - { - // Count ERC20 and ERC721 tokens from whitelistedTokenList (excluding ERC1155) - uint256 erc20AndErc721Count = 0; - for (uint256 i = 0; i < whitelistedTokenList.length; i++) { - LibItems.RewardType tokenType = tokenTypes[whitelistedTokenList[i]]; - if (tokenType == LibItems.RewardType.ERC20 || tokenType == LibItems.RewardType.ERC721) { - erc20AndErc721Count++; - } - } - - // Count unique ERC1155 token IDs (since one ERC1155 contract can have multiple token IDs) - uint256 erc1155Count = _countUniqueErc1155TokenIds(); - uint256 totalCount = erc20AndErc721Count + erc1155Count; - - addresses = new address[](totalCount); - totalBalances = new uint256[](totalCount); - reservedBalances = new uint256[](totalCount); - availableBalances = new uint256[](totalCount); - symbols = new string[](totalCount); - names = new string[](totalCount); - types = new string[](totalCount); - - uint256 currentIndex = 0; - - // Process all whitelisted tokens - for (uint256 i = 0; i < whitelistedTokenList.length; i++) { - address tokenAddress = whitelistedTokenList[i]; - LibItems.RewardType tokenType = tokenTypes[tokenAddress]; - - addresses[currentIndex] = tokenAddress; - - if (tokenType == LibItems.RewardType.ERC20) { - // ERC20 token - uint256 totalBalance = IERC20(tokenAddress).balanceOf(address(this)); - uint256 reserved = reservedAmounts[tokenAddress]; - - totalBalances[currentIndex] = totalBalance; - reservedBalances[currentIndex] = reserved; - availableBalances[currentIndex] = totalBalance > reserved ? totalBalance - reserved : 0; - - try IERC20Metadata(tokenAddress).symbol() returns (string memory symbol) { - symbols[currentIndex] = symbol; - } catch { - symbols[currentIndex] = "UNKNOWN"; - } - - try IERC20Metadata(tokenAddress).name() returns (string memory name) { - names[currentIndex] = name; - } catch { - names[currentIndex] = "Unknown Token"; - } - - types[currentIndex] = "fa"; - currentIndex++; - - } else if (tokenType == LibItems.RewardType.ERC721) { - // ERC721 token - uint256 totalBalance = IERC721(tokenAddress).balanceOf(address(this)); - uint256 reserved = erc721TotalReserved[tokenAddress]; - - totalBalances[currentIndex] = totalBalance; - reservedBalances[currentIndex] = reserved; - availableBalances[currentIndex] = totalBalance > reserved ? totalBalance - reserved : 0; - - // Try to get ERC721 metadata - try IERC721Metadata(tokenAddress).symbol() returns (string memory symbol) { - symbols[currentIndex] = symbol; - } catch { - symbols[currentIndex] = "ERC721"; - } - - try IERC721Metadata(tokenAddress).name() returns (string memory name) { - names[currentIndex] = name; - } catch { - names[currentIndex] = "NFT Collection"; - } - - types[currentIndex] = "nft"; - currentIndex++; - - } else if (tokenType == LibItems.RewardType.ERC1155) { - // ERC1155 tokens - need to iterate through rewards to get token IDs - // We'll handle these separately below - continue; - } - } - - // Process ERC1155 tokens separately (since they have multiple token IDs per contract) - // Track processed ERC1155 combinations to avoid duplicates - address[] memory processedErc1155Addresses = new address[](erc1155Count); - uint256[] memory processedErc1155TokenIds = new uint256[](erc1155Count); - uint256 processedCount = 0; - - for (uint256 i = 0; i < itemIds.length; i++) { - uint256 tokenId = itemIds[i]; - LibItems.RewardToken storage rewardToken = tokenRewards[tokenId]; - - for (uint256 j = 0; j < rewardToken.rewards.length; j++) { - LibItems.Reward storage reward = rewardToken.rewards[j]; - - if (reward.rewardType != LibItems.RewardType.ERC1155) { - continue; - } - - address erc1155Address = reward.rewardTokenAddress; - uint256 erc1155TokenId = reward.rewardTokenId; - - // Check if this exact address+tokenID combination was already added - bool alreadyAdded = false; - for (uint256 k = 0; k < processedCount; k++) { - if (processedErc1155Addresses[k] == erc1155Address && - processedErc1155TokenIds[k] == erc1155TokenId) { - alreadyAdded = true; - break; - } - } - - if (!alreadyAdded && currentIndex < totalCount) { - // Track this combination - processedErc1155Addresses[processedCount] = erc1155Address; - processedErc1155TokenIds[processedCount] = erc1155TokenId; - processedCount++; - - addresses[currentIndex] = erc1155Address; - - uint256 balance = IERC1155(erc1155Address).balanceOf(address(this), erc1155TokenId); - uint256 reserved = erc1155ReservedAmounts[erc1155Address][erc1155TokenId]; - - totalBalances[currentIndex] = balance; - reservedBalances[currentIndex] = reserved; - availableBalances[currentIndex] = balance > reserved ? balance - reserved : 0; - - // ERC1155 standard does not include name() or symbol() functions - // Use generic names for ERC1155 tokens - names[currentIndex] = "ERC1155 Collection"; - symbols[currentIndex] = "ERC1155"; - types[currentIndex] = "nft"; - currentIndex++; - } - } - } + function getAllItemIds() external view returns (uint256[] memory) { + return _state().getAllItemIds(); } - + /** - * @dev Count unique ERC1155 token IDs used in rewards. - * ERC1155 contracts can have multiple token IDs, so we need to count them separately. + * @dev Get the rewards array for a given tokenId. + * @param _tokenId The ID of the reward token. + * @return The rewards array. */ - function _countUniqueErc1155TokenIds() private view returns (uint256) { - // Use a large enough array to track unique combinations - address[] memory uniqueAddresses = new address[](itemIds.length * 10); - uint256[] memory uniqueTokenIds = new uint256[](itemIds.length * 10); - uint256 count = 0; - - for (uint256 i = 0; i < itemIds.length; i++) { - uint256 tokenId = itemIds[i]; - LibItems.RewardToken storage rewardToken = tokenRewards[tokenId]; - - for (uint256 j = 0; j < rewardToken.rewards.length; j++) { - LibItems.Reward storage reward = rewardToken.rewards[j]; - - if (reward.rewardType == LibItems.RewardType.ERC1155) { - address erc1155Address = reward.rewardTokenAddress; - uint256 erc1155TokenId = reward.rewardTokenId; - - // Check if this combination already exists - bool found = false; - for (uint256 k = 0; k < count; k++) { - if (uniqueAddresses[k] == erc1155Address && uniqueTokenIds[k] == erc1155TokenId) { - found = true; - break; - } - } - - if (!found) { - uniqueAddresses[count] = erc1155Address; - uniqueTokenIds[count] = erc1155TokenId; - count++; - } - } - } - } - - return count; - } - - - function getAllItemIds() external view returns (uint256[] memory) { - return itemIds; + function getTokenRewards(uint256 _tokenId) external view returns (LibItems.Reward[] memory) { + return _state().getRewardToken(_tokenId).rewards; } function decodeData( @@ -529,22 +312,22 @@ contract Rewards is for (uint256 i = 0; i < _token.rewards.length; i++) { LibItems.Reward memory reward = _token.rewards[i]; if (reward.rewardType == LibItems.RewardType.ERC20) { - if (!whitelistedTokens[reward.rewardTokenAddress]) { + if (!_state().whitelistedTokens(reward.rewardTokenAddress)) { revert TokenNotWhitelisted(); } uint256 totalAmount = reward.rewardAmount * _token.maxSupply; uint256 balance = IERC20(reward.rewardTokenAddress).balanceOf( address(this) ); - uint256 reserved = reservedAmounts[reward.rewardTokenAddress]; + uint256 reserved = _state().reservedAmounts(reward.rewardTokenAddress); if (balance < reserved + totalAmount) { revert InsufficientTreasuryBalance(); } // Reserve the amount - reservedAmounts[reward.rewardTokenAddress] += totalAmount; + _state().increaseERC20Reserved(reward.rewardTokenAddress, totalAmount); } else if (reward.rewardType == LibItems.RewardType.ERC721) { // Validate token is whitelisted - if (!whitelistedTokens[reward.rewardTokenAddress]) { + if (!_state().whitelistedTokens(reward.rewardTokenAddress)) { revert TokenNotWhitelisted(); } IERC721 nftContract = IERC721(reward.rewardTokenAddress); @@ -552,38 +335,32 @@ contract Rewards is for (uint256 j = 0; j < reward.rewardTokenIds.length; j++) { uint256 tokenId = reward.rewardTokenIds[j]; // Check contract owns this NFT and it is not already reserved - if (nftContract.ownerOf(tokenId) != address(this) || isErc721Reserved[reward.rewardTokenAddress][tokenId]) { + if (nftContract.ownerOf(tokenId) != address(this) || _state().isErc721Reserved(reward.rewardTokenAddress, tokenId)) { revert InsufficientTreasuryBalance(); } } // Reserve all tokenIds for (uint256 j = 0; j < reward.rewardTokenIds.length; j++) { uint256 tokenId = reward.rewardTokenIds[j]; - isErc721Reserved[reward.rewardTokenAddress][tokenId] = true; - erc721TotalReserved[reward.rewardTokenAddress]++; + _state().reserveERC721(reward.rewardTokenAddress, tokenId); } } else if (reward.rewardType == LibItems.RewardType.ERC1155) { // Validate token is whitelisted - if (!whitelistedTokens[reward.rewardTokenAddress]) { + if (!_state().whitelistedTokens(reward.rewardTokenAddress)) { revert TokenNotWhitelisted(); } uint256 totalAmount = reward.rewardAmount * _token.maxSupply; uint256 balance = IERC1155(reward.rewardTokenAddress).balanceOf(address(this), reward.rewardTokenId); - uint256 reserved = erc1155ReservedAmounts[reward.rewardTokenAddress][reward.rewardTokenId]; + uint256 reserved = _state().erc1155ReservedAmounts(reward.rewardTokenAddress, reward.rewardTokenId); if (balance < reserved + totalAmount) { revert InsufficientTreasuryBalance(); } // Reserve the amount - erc1155ReservedAmounts[reward.rewardTokenAddress][ - reward.rewardTokenId - ] += totalAmount; - erc1155TotalReserved[reward.rewardTokenAddress] += totalAmount; + _state().increaseERC1155Reserved(reward.rewardTokenAddress, reward.rewardTokenId, totalAmount); } } - tokenRewards[_token.tokenId] = _token; - tokenExists[_token.tokenId] = true; - itemIds.push(_token.tokenId); + _state().addRewardToken(_token.tokenId, _token); rewardTokenContract.addNewToken(_token.tokenId); emit TokenAdded(_token.tokenId); @@ -626,268 +403,145 @@ contract Rewards is uint256 _tokenId, bool _isTokenMintPaused ) public onlyRole(MANAGER_ROLE) { - isTokenMintPaused[_tokenId] = _isTokenMintPaused; - emit TokenMintPausedUpdated(_tokenId, _isTokenMintPaused); + _state().setTokenMintPaused(_tokenId, _isTokenMintPaused); } function updateClaimRewardPaused( uint256 _tokenId, bool _isClaimRewardPaused ) public onlyRole(MANAGER_ROLE) { - isClaimRewardPaused[_tokenId] = _isClaimRewardPaused; - emit ClaimRewardPausedUpdated(_tokenId, _isClaimRewardPaused); + _state().setClaimRewardPaused(_tokenId, _isClaimRewardPaused); } /*////////////////////////////////////////////////////////////// TREASURY MANAGEMENT //////////////////////////////////////////////////////////////*/ - /** - * @dev Whitelist a token for use in the treasury system. - * @param _token The address of the token to whitelist. - * @param _type The type of the token (ERC20, ERC721, ERC1155). - */ - function whitelistToken( - address _token, - LibItems.RewardType _type - ) external onlyRole(MANAGER_ROLE) { - if (_token == address(0)) { - revert AddressIsZero(); - } - if (whitelistedTokens[_token]) { - revert TokenAlreadyWhitelisted(); - } - whitelistedTokens[_token] = true; - tokenTypes[_token] = _type; - whitelistedTokenList.push(_token); - - if (_type == LibItems.RewardType.ERC20) { - reservedAmounts[_token] = 0; - } - - emit TokenWhitelisted(_token); + // Delegate to Treasury contract for all treasury management + function whitelistToken(address _token, LibItems.RewardType _type) external onlyRole(MANAGER_ROLE) { + Treasury(treasury).whitelistToken(_token, _type); } - /** - * @dev Remove a token from the whitelist. - * @param _token The address of the token to remove. - */ - function removeTokenFromWhitelist( - address _token - ) external onlyRole(MANAGER_ROLE) { - if (!whitelistedTokens[_token]) { - revert TokenNotWhitelisted(); - } + function removeTokenFromWhitelist(address _token) external onlyRole(MANAGER_ROLE) { + Treasury(treasury).removeTokenFromWhitelist(_token); + } - LibItems.RewardType _type = tokenTypes[_token]; - - if (_type == LibItems.RewardType.ERC20) { - // Ensure no reserved amounts before removing - if (reservedAmounts[_token] > 0) { - revert TokenHasReserves(); - } - // Ensure contract has no balance for this token - uint256 balance = IERC20(_token).balanceOf(address(this)); - if (balance > 0) { - revert TokenHasReserves(); - } - } else if (_type == LibItems.RewardType.ERC721) { - // Ensure no reserved amounts before removing - if (erc721TotalReserved[_token] > 0) { - revert TokenHasReserves(); - } - // Ensure contract has no balance for this token - uint256 balance = IERC721(_token).balanceOf(address(this)); - if (balance > 0) { - revert TokenHasReserves(); - } - } else if (_type == LibItems.RewardType.ERC1155) { - // Ensure no reserved amounts before removing - if (erc1155TotalReserved[_token] > 0) { - revert TokenHasReserves(); - } - } + function depositToTreasury(address _token, uint256 _amount) external { + Treasury(treasury).depositToTreasury(_token, _amount, _msgSender()); + } - whitelistedTokens[_token] = false; + function withdrawUnreservedTreasury(address _token, address _to) external onlyRole(MANAGER_ROLE) { + Treasury(treasury).withdrawUnreservedTreasury(_token, _to); + } - // Remove from list - for (uint256 i = 0; i < whitelistedTokenList.length; i++) { - if (whitelistedTokenList[i] == _token) { - whitelistedTokenList[i] = whitelistedTokenList[ - whitelistedTokenList.length - 1 - ]; - whitelistedTokenList.pop(); - break; - } - } - emit TokenRemovedFromWhitelist(_token); + function withdrawERC721UnreservedTreasury(address _token, address _to, uint256 _tokenId) external onlyRole(MANAGER_ROLE) { + Treasury(treasury).withdrawERC721UnreservedTreasury(_token, _to, _tokenId); } - /** - * @dev Deposit tokens to the treasury. - * @param _token The address of the ERC20 token to deposit. - * @param _amount The amount to deposit. - */ - function depositToTreasury(address _token, uint256 _amount) external { - if (!whitelistedTokens[_token]) { - revert TokenNotWhitelisted(); - } - if (_amount == 0) { - revert InvalidAmount(); - } - SafeERC20.safeTransferFrom( - IERC20(_token), - _msgSender(), - address(this), - _amount - ); - emit TreasuryDeposit(_token, _amount); + function withdrawERC1155UnreservedTreasury(address _token, address _to, uint256 _tokenId, uint256 _amount) external onlyRole(MANAGER_ROLE) { + Treasury(treasury).withdrawERC1155UnreservedTreasury(_token, _to, _tokenId, _amount); } + // - getAllTreasuryBalances() + // - getTreasuryBalance() + // - getReservedAmount() + // - getAvailableTreasuryBalance() + // - getWhitelistedTokens() + // - isWhitelistedToken() /** - * @dev Withdraw unreserved tokens from the treasury. - * @param _token The address of the ERC20 token to withdraw. - * @param _to The address to send the tokens to. + * @dev Get treasury balances for all whitelisted tokens with full balance breakdown. + * Delegates to external Treasury contract to reduce contract size. + * @return addresses Array of token addresses. + * @return totalBalances Array of total balances in the contract. + * @return reservedBalances Array of reserved amounts for rewards. + * @return availableBalances Array of available (unreserved) balances. + * @return symbols Array of token symbols. + * @return names Array of token names. + * @return types Array of token types ("fa" for fungible assets, "nft" for NFTs). */ - function withdrawUnreservedTreasury( - address _token, - address _to - ) external onlyRole(MANAGER_ROLE) { - if (_to == address(0)) { - revert AddressIsZero(); - } - if (!whitelistedTokens[_token]) { - revert TokenNotWhitelisted(); - } - - uint256 balance = IERC20(_token).balanceOf(address(this)); - uint256 reserved = reservedAmounts[_token]; - - if (balance <= reserved) { - revert InsufficientBalance(); - } - - uint256 withdrawable = balance - reserved; - SafeERC20.safeTransfer(IERC20(_token), _to, withdrawable); + function getAllTreasuryBalances() + external + view + returns ( + address[] memory addresses, + uint256[] memory totalBalances, + uint256[] memory reservedBalances, + uint256[] memory availableBalances, + string[] memory symbols, + string[] memory names, + string[] memory types + ) + { + return Treasury(treasury).getAllTreasuryBalances(address(this)); } /** - * @dev Withdraw unreserved ERC721 tokens from the treasury. - * @param _token The address of the ERC721 token to withdraw. - * @param _to The address to send the tokens to. - * @param _tokenId The token ID to withdraw. + * @dev Set the address of the Treasury contract + * @param _treasuryContract The address of the deployed Treasury contract */ - function withdrawERC721UnreservedTreasury( - address _token, - address _to, - uint256 _tokenId - ) external onlyRole(MANAGER_ROLE) { - if (_to == address(0)) { - revert AddressIsZero(); - } - if (!whitelistedTokens[_token]) { - revert TokenNotWhitelisted(); - } - - if (isErc721Reserved[_token][_tokenId]) { - revert InsufficientTreasuryBalance(); - } - - // This will revert if we don't own it - if (IERC721(_token).ownerOf(_tokenId) != address(this)) { - revert InsufficientBalance(); - } - - IERC721(_token).safeTransferFrom(address(this), _to, _tokenId); + function setTreasury(address _treasuryContract) external onlyRole(DEV_CONFIG_ROLE) { + treasury = _treasuryContract; } /** - * @dev Withdraw unreserved ERC1155 tokens from the treasury. - * @param _token The address of the ERC1155 token to withdraw. - * @param _to The address to send the tokens to. - * @param _tokenId The token ID to withdraw. - * @param _amount The amount to withdraw. + * @dev Set the address of the RewardsState contract + * @param _rewardsStateContract The address of the deployed RewardsState contract */ - function withdrawERC1155UnreservedTreasury( - address _token, - address _to, - uint256 _tokenId, - uint256 _amount - ) external onlyRole(MANAGER_ROLE) { - if (_to == address(0)) { - revert AddressIsZero(); - } - if (!whitelistedTokens[_token]) { - revert TokenNotWhitelisted(); - } - - uint256 balance = IERC1155(_token).balanceOf(address(this), _tokenId); - uint256 reserved = erc1155ReservedAmounts[_token][_tokenId]; - - if (balance <= reserved) { - revert InsufficientBalance(); - } - - uint256 withdrawable = balance - reserved; - - if (_amount > withdrawable) { - revert InsufficientBalance(); - } - - IERC1155(_token).safeTransferFrom(address(this), _to, _tokenId, _amount, ""); + function setRewardsState(address _rewardsStateContract) external onlyRole(DEV_CONFIG_ROLE) { + rewardsState = _rewardsStateContract; } /** * @dev Get the treasury balance for a token. + * Delegates to external Treasury contract. * @param _token The address of the ERC20 token. * @return The balance of the token in the treasury. */ function getTreasuryBalance( address _token ) external view returns (uint256) { - return IERC20(_token).balanceOf(address(this)); + return Treasury(treasury).getTreasuryBalance(address(this), _token); } /** * @dev Get the reserved amount for a token. + * Delegates to external Treasury contract. * @param _token The address of the ERC20 token. * @return The reserved amount of the token. */ function getReservedAmount(address _token) external view returns (uint256) { - return reservedAmounts[_token]; + return Treasury(treasury).getReservedAmount(address(this), _token); } /** * @dev Get the available (unreserved) treasury balance for a token. + * Delegates to external Treasury contract. * @param _token The address of the ERC20 token. * @return The available balance. */ function getAvailableTreasuryBalance( address _token ) external view returns (uint256) { - uint256 balance = IERC20(_token).balanceOf(address(this)); - uint256 reserved = reservedAmounts[_token]; - if (balance <= reserved) { - return 0; - } - return balance - reserved; + return Treasury(treasury).getAvailableTreasuryBalance(address(this), _token); } /** * @dev Get all whitelisted tokens. + * Delegates to external Treasury contract. * @return Array of whitelisted token addresses. */ function getWhitelistedTokens() external view returns (address[] memory) { - return whitelistedTokenList; + return Treasury(treasury).getWhitelistedTokens(address(this)); } /** * @dev Check if a token is whitelisted. + * Delegates to external Treasury contract. * @param _token The address of the ERC20 token. * @return True if whitelisted, false otherwise. */ function isWhitelistedToken(address _token) external view returns (bool) { - return whitelistedTokens[_token]; + return Treasury(treasury).isWhitelistedToken(address(this), _token); } /*////////////////////////////////////////////////////////////// @@ -910,7 +564,7 @@ contract Rewards is revert InvalidAmount(); } - LibItems.RewardToken storage rewardToken = tokenRewards[_tokenId]; + LibItems.RewardToken memory rewardToken = _state().getRewardToken(_tokenId); uint256 oldSupply = rewardToken.maxSupply; uint256 newSupply = oldSupply + _additionalSupply; @@ -922,59 +576,17 @@ contract Rewards is uint256 balance = IERC20(reward.rewardTokenAddress).balanceOf( address(this) ); - uint256 reserved = reservedAmounts[reward.rewardTokenAddress]; + uint256 reserved = _state().reservedAmounts(reward.rewardTokenAddress); if (balance < reserved + additionalAmount) { revert InsufficientTreasuryBalance(); } // Reserve additional amount - reservedAmounts[reward.rewardTokenAddress] += additionalAmount; - } - } - - rewardToken.maxSupply = newSupply; - emit RewardSupplyChanged(_tokenId, oldSupply, newSupply); - } - - /** - * @dev Reduce the max supply of a reward token. - * @param _tokenId The ID of the reward token. - * @param _reduceBy The amount to reduce the supply by. - */ - function reduceRewardSupply( - uint256 _tokenId, - uint256 _reduceBy - ) external onlyRole(MANAGER_ROLE) { - if (!isTokenExist(_tokenId)) { - revert TokenNotExist(); - } - if (_reduceBy == 0) { - revert InvalidAmount(); - } - - LibItems.RewardToken storage rewardToken = tokenRewards[_tokenId]; - uint256 oldSupply = rewardToken.maxSupply; - - // Ensure we don't reduce below current supply (already minted) - if (oldSupply < _reduceBy) { - revert CannotReduceSupply(); - } - uint256 newSupply = oldSupply - _reduceBy; - if (newSupply < currentRewardSupply[_tokenId]) { - revert CannotReduceSupply(); - } - - // Release reserved amounts for ERC20 rewards - for (uint256 i = 0; i < rewardToken.rewards.length; i++) { - LibItems.Reward memory reward = rewardToken.rewards[i]; - if (reward.rewardType == LibItems.RewardType.ERC20) { - uint256 releaseAmount = reward.rewardAmount * _reduceBy; - if (reservedAmounts[reward.rewardTokenAddress] >= releaseAmount) { - reservedAmounts[reward.rewardTokenAddress] -= releaseAmount; - } + _state().increaseERC20Reserved(reward.rewardTokenAddress, additionalAmount); } } rewardToken.maxSupply = newSupply; + _state().updateRewardToken(_tokenId, rewardToken); emit RewardSupplyChanged(_tokenId, oldSupply, newSupply); } @@ -994,7 +606,9 @@ contract Rewards is if (!isTokenExist(_tokenId)) { revert TokenNotExist(); } - tokenRewards[_tokenId].tokenUri = _newUri; + LibItems.RewardToken memory rewardToken = _state().getRewardToken(_tokenId); + rewardToken.tokenUri = _newUri; + _state().updateRewardToken(_tokenId, rewardToken); emit TokenURIChanged(_tokenId, _newUri); } @@ -1023,9 +637,9 @@ contract Rewards is _transferEther(payable(_to), _amounts[0]); } else if (_rewardType == LibItems.RewardType.ERC20) { // Check if withdrawal would violate reserved amounts - if (whitelistedTokens[_tokenAddress]) { + if (_state().whitelistedTokens(_tokenAddress)) { uint256 balance = IERC20(_tokenAddress).balanceOf(address(this)); - uint256 reserved = reservedAmounts[_tokenAddress]; + uint256 reserved = _state().reservedAmounts(_tokenAddress); if (balance < reserved + _amounts[0]) { revert InsufficientTreasuryBalance(); } @@ -1035,7 +649,7 @@ contract Rewards is IERC721 token = IERC721(_tokenAddress); for (uint256 i = 0; i < _tokenIds.length; i++) { // Check if NFT is reserved - if (isErc721Reserved[_tokenAddress][_tokenIds[i]]) { + if (_state().isErc721Reserved(_tokenAddress, _tokenIds[i])) { revert InsufficientTreasuryBalance(); } _transferERC721(token, _from, _to, _tokenIds[i]); @@ -1047,7 +661,7 @@ contract Rewards is for (uint256 i = 0; i < _tokenIds.length; i++) { // Check if amount exceeds unreserved balance uint256 balance = IERC1155(_tokenAddress).balanceOf(address(this), _tokenIds[i]); - uint256 reserved = erc1155ReservedAmounts[_tokenAddress][_tokenIds[i]]; + uint256 reserved = _state().erc1155ReservedAmounts(_tokenAddress, _tokenIds[i]); uint256 available = balance > reserved ? balance - reserved : 0; if (_amounts[i] > available) { revert InsufficientTreasuryBalance(); @@ -1133,7 +747,7 @@ contract Rewards is } function _claimReward(address _to, uint256 _rewardTokenId) private { - if (isClaimRewardPaused[_rewardTokenId]) { + if (_state().isClaimRewardPaused(_rewardTokenId)) { revert ClaimRewardPaused(); } @@ -1153,7 +767,7 @@ contract Rewards is } function _distributeReward(address _to, uint256 _rewardTokenId) private { - LibItems.RewardToken memory _rewardToken = tokenRewards[_rewardTokenId]; + LibItems.RewardToken memory _rewardToken = _state().getRewardToken(_rewardTokenId); LibItems.Reward[] memory rewards = _rewardToken.rewards; for (uint256 i = 0; i < rewards.length; i++) { @@ -1169,13 +783,9 @@ contract Rewards is reward.rewardAmount ); // Reduce reserved amount - if (reservedAmounts[reward.rewardTokenAddress] >= reward.rewardAmount) { - reservedAmounts[reward.rewardTokenAddress] -= reward.rewardAmount; - } + _state().decreaseERC20Reserved(reward.rewardTokenAddress, reward.rewardAmount); } else if (reward.rewardType == LibItems.RewardType.ERC721) { - uint256 currentIndex = erc721RewardCurrentIndex[_rewardTokenId][ - i - ]; + uint256 currentIndex = _state().getERC721RewardCurrentIndex(_rewardTokenId, i); uint256[] memory tokenIds = reward.rewardTokenIds; for (uint256 j = 0; j < reward.rewardAmount; j++) { if (currentIndex + j >= tokenIds.length) { @@ -1184,8 +794,7 @@ contract Rewards is uint256 tokenId = tokenIds[currentIndex + j]; // Release reservation - isErc721Reserved[reward.rewardTokenAddress][tokenId] = false; - erc721TotalReserved[reward.rewardTokenAddress]--; + _state().releaseERC721(reward.rewardTokenAddress, tokenId); _transferERC721( IERC721(reward.rewardTokenAddress), @@ -1195,17 +804,10 @@ contract Rewards is ); } - erc721RewardCurrentIndex[_rewardTokenId][i] += reward.rewardAmount; + _state().incrementERC721RewardIndex(_rewardTokenId, i); } else if (reward.rewardType == LibItems.RewardType.ERC1155) { // Release reservation - if (erc1155ReservedAmounts[reward.rewardTokenAddress][reward.rewardTokenId] >= reward.rewardAmount) { - erc1155ReservedAmounts[reward.rewardTokenAddress][ - reward.rewardTokenId - ] -= reward.rewardAmount; - if (erc1155TotalReserved[reward.rewardTokenAddress] >= reward.rewardAmount) { - erc1155TotalReserved[reward.rewardTokenAddress] -= reward.rewardAmount; - } - } + _state().decreaseERC1155Reserved(reward.rewardTokenAddress, reward.rewardTokenId, reward.rewardAmount); _transferERC1155( IERC1155(reward.rewardTokenAddress), @@ -1253,7 +855,7 @@ contract Rewards is revert TokenNotExist(); } - if (isTokenMintPaused[_tokenId]) { + if (_state().isTokenMintPaused(_tokenId)) { revert MintPaused(); } @@ -1261,11 +863,11 @@ contract Rewards is revert InvalidAmount(); } - uint256 newSupply = currentRewardSupply[_tokenId] + _amount; - if (newSupply > tokenRewards[_tokenId].maxSupply) { + uint256 newSupply = _state().currentRewardSupply(_tokenId) + _amount; + if (newSupply > _state().getRewardToken(_tokenId).maxSupply) { revert ExceedMaxSupply(); } - currentRewardSupply[_tokenId] = newSupply; + _state().increaseCurrentSupply(_tokenId, _amount); // claim the reward if (isClaimReward) { @@ -1317,9 +919,10 @@ contract Rewards is uint256[] memory rewardTokenId ) { - tokenUri = tokenRewards[tokenId].tokenUri; - maxSupply = tokenRewards[tokenId].maxSupply; - LibItems.Reward[] memory rewards = tokenRewards[tokenId].rewards; + LibItems.RewardToken memory rewardToken = _state().getRewardToken(tokenId); + tokenUri = rewardToken.tokenUri; + maxSupply = rewardToken.maxSupply; + LibItems.Reward[] memory rewards = rewardToken.rewards; rewardTypes = new LibItems.RewardType[](rewards.length); rewardAmounts = new uint256[](rewards.length); @@ -1366,7 +969,7 @@ contract Rewards is if (!isTokenExist(_tokenId)) { return false; } - if (isClaimRewardPaused[_tokenId]) { + if (_state().isClaimRewardPaused(_tokenId)) { return false; } return rewardTokenContract.balanceOf(_user, _tokenId) > 0; @@ -1386,7 +989,7 @@ contract Rewards is if (!isTokenExist(_tokenId)) { return (0, 0); } - LibItems.RewardToken memory rewardToken = tokenRewards[_tokenId]; + LibItems.RewardToken memory rewardToken = _state().getRewardToken(_tokenId); if (_rewardIndex >= rewardToken.rewards.length) { return (0, 0); } @@ -1394,7 +997,7 @@ contract Rewards is if (reward.rewardType != LibItems.RewardType.ERC721) { return (0, 0); } - distributed = erc721RewardCurrentIndex[_tokenId][_rewardIndex]; + distributed = _state().getERC721RewardCurrentIndex(_tokenId, _rewardIndex); total = reward.rewardTokenIds.length; } @@ -1409,8 +1012,8 @@ contract Rewards is if (!isTokenExist(_tokenId)) { return 0; } - uint256 maxSupply = tokenRewards[_tokenId].maxSupply; - uint256 current = currentRewardSupply[_tokenId]; + uint256 maxSupply = _state().getRewardToken(_tokenId).maxSupply; + uint256 current = _state().currentRewardSupply(_tokenId); if (current >= maxSupply) { return 0; } @@ -1427,7 +1030,7 @@ contract Rewards is address _user, uint256 _nonce ) external view returns (bool) { - return userNonces[_user][_nonce]; + return _state().userNonces(_user, _nonce); } /** @@ -1452,11 +1055,11 @@ contract Rewards is { // Check user nonce not already used address user = _msgSender(); - if (userNonces[user][nonce]) { + if (_state().userNonces(user, nonce)) { revert NonceAlreadyUsed(); } // Mark nonce as used - userNonces[user][nonce] = true; + _state().setUserNonce(user, nonce, true); uint256[] memory _tokenIds = _verifyContractChainIdAndDecode(data); _mintAndClaimRewardTokenBatch( diff --git a/contracts/upgradeables/soulbounds/RewardsState.sol b/contracts/upgradeables/soulbounds/RewardsState.sol new file mode 100644 index 0000000..ea2e69f --- /dev/null +++ b/contracts/upgradeables/soulbounds/RewardsState.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +// @author Summon.xyz Team - https://summon.xyz +// @contributors: [ @ogarciarevett, @karacurt] + +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import { LibItems } from "../../libraries/LibItems.sol"; + +/** + * @title RewardsState + * @notice Centralized state storage for the Rewards system + * @dev This contract holds all state that is shared between Rewards and Treasury contracts. + * Only authorized contracts (with STATE_MANAGER_ROLE) can modify state. + * This contract is upgradeable using the UUPS pattern. + */ +contract RewardsState is Initializable, AccessControlUpgradeable, UUPSUpgradeable { + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + error AddressIsZero(); + error TokenAlreadyWhitelisted(); + error TokenNotWhitelisted(); + + /*////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + bytes32 public constant STATE_MANAGER_ROLE = keccak256("STATE_MANAGER_ROLE"); + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + // Treasury whitelist + mapping(address => bool) public whitelistedTokens; + address[] private whitelistedTokenList; + mapping(address => LibItems.RewardType) public tokenTypes; + + // ERC20 Reservations + mapping(address => uint256) public reservedAmounts; + + // ERC721 Reservations + mapping(address => mapping(uint256 => bool)) public isErc721Reserved; + mapping(address => uint256) public erc721TotalReserved; + + // ERC1155 Reservations + mapping(address => mapping(uint256 => uint256)) public erc1155ReservedAmounts; + mapping(address => uint256) public erc1155TotalReserved; + + // Reward Token Management + uint256[] public itemIds; + mapping(uint256 => bool) public tokenExists; + mapping(uint256 => LibItems.RewardToken) public tokenRewards; + mapping(uint256 => bool) public isTokenMintPaused; + mapping(uint256 => bool) public isClaimRewardPaused; + mapping(uint256 => mapping(uint256 => uint256)) public erc721RewardCurrentIndex; // rewardTokenId => rewardIndex => erc721RewardCurrentIndex + mapping(uint256 => uint256) public currentRewardSupply; + + // Per-user nonce tracking + mapping(address => mapping(uint256 => bool)) public userNonces; + + uint256[50] private __gap; + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + event TokenWhitelisted(address indexed token, LibItems.RewardType tokenType); + event TokenRemovedFromWhitelist(address indexed token); + event RewardTokenAdded(uint256 indexed tokenId); + event RewardTokenUpdated(uint256 indexed tokenId); + event TokenMintPausedUpdated(uint256 indexed tokenId, bool isPaused); + event ClaimRewardPausedUpdated(uint256 indexed tokenId, bool isPaused); + event UserNonceUsed(address indexed user, uint256 indexed nonce); + + /*////////////////////////////////////////////////////////////// + INITIALIZER + //////////////////////////////////////////////////////////////*/ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address _admin) external initializer { + if (_admin == address(0)) revert AddressIsZero(); + + __AccessControl_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(UPGRADER_ROLE, _admin); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} + + /*////////////////////////////////////////////////////////////// + WHITELIST MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + function whitelistToken(address _token, LibItems.RewardType _type) external onlyRole(STATE_MANAGER_ROLE) { + if (_token == address(0)) revert AddressIsZero(); + if (whitelistedTokens[_token]) revert TokenAlreadyWhitelisted(); + + whitelistedTokens[_token] = true; + tokenTypes[_token] = _type; + whitelistedTokenList.push(_token); + + if (_type == LibItems.RewardType.ERC20) { + reservedAmounts[_token] = 0; + } + + emit TokenWhitelisted(_token, _type); + } + + function removeTokenFromWhitelist(address _token) external onlyRole(STATE_MANAGER_ROLE) { + if (!whitelistedTokens[_token]) revert TokenNotWhitelisted(); + + whitelistedTokens[_token] = false; + + // Remove from list + for (uint256 i = 0; i < whitelistedTokenList.length; i++) { + if (whitelistedTokenList[i] == _token) { + whitelistedTokenList[i] = whitelistedTokenList[whitelistedTokenList.length - 1]; + whitelistedTokenList.pop(); + break; + } + } + + emit TokenRemovedFromWhitelist(_token); + } + + function getWhitelistedTokens() external view returns (address[] memory) { + return whitelistedTokenList; + } + + /*////////////////////////////////////////////////////////////// + RESERVATION MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + function increaseERC20Reserved(address _token, uint256 _amount) external onlyRole(STATE_MANAGER_ROLE) { + reservedAmounts[_token] += _amount; + } + + function decreaseERC20Reserved(address _token, uint256 _amount) external onlyRole(STATE_MANAGER_ROLE) { + if (reservedAmounts[_token] >= _amount) { + reservedAmounts[_token] -= _amount; + } + } + + function reserveERC721(address _token, uint256 _tokenId) external onlyRole(STATE_MANAGER_ROLE) { + isErc721Reserved[_token][_tokenId] = true; + erc721TotalReserved[_token]++; + } + + function releaseERC721(address _token, uint256 _tokenId) external onlyRole(STATE_MANAGER_ROLE) { + isErc721Reserved[_token][_tokenId] = false; + if (erc721TotalReserved[_token] > 0) { + erc721TotalReserved[_token]--; + } + } + + function increaseERC1155Reserved(address _token, uint256 _tokenId, uint256 _amount) external onlyRole(STATE_MANAGER_ROLE) { + erc1155ReservedAmounts[_token][_tokenId] += _amount; + erc1155TotalReserved[_token] += _amount; + } + + function decreaseERC1155Reserved(address _token, uint256 _tokenId, uint256 _amount) external onlyRole(STATE_MANAGER_ROLE) { + if (erc1155ReservedAmounts[_token][_tokenId] >= _amount) { + erc1155ReservedAmounts[_token][_tokenId] -= _amount; + } + if (erc1155TotalReserved[_token] >= _amount) { + erc1155TotalReserved[_token] -= _amount; + } + } + + /*////////////////////////////////////////////////////////////// + REWARD TOKEN MANAGEMENT + //////////////////////////////////////////////////////////////*/ + + function addRewardToken(uint256 _tokenId, LibItems.RewardToken memory _rewardToken) external onlyRole(STATE_MANAGER_ROLE) { + if (tokenExists[_tokenId]) revert TokenAlreadyWhitelisted(); + + tokenExists[_tokenId] = true; + tokenRewards[_tokenId] = _rewardToken; + itemIds.push(_tokenId); + currentRewardSupply[_tokenId] = 0; + + emit RewardTokenAdded(_tokenId); + } + + function updateRewardToken(uint256 _tokenId, LibItems.RewardToken memory _rewardToken) external onlyRole(STATE_MANAGER_ROLE) { + if (!tokenExists[_tokenId]) revert TokenNotWhitelisted(); + tokenRewards[_tokenId] = _rewardToken; + emit RewardTokenUpdated(_tokenId); + } + + function setTokenMintPaused(uint256 _tokenId, bool _isPaused) external onlyRole(STATE_MANAGER_ROLE) { + isTokenMintPaused[_tokenId] = _isPaused; + emit TokenMintPausedUpdated(_tokenId, _isPaused); + } + + function setClaimRewardPaused(uint256 _tokenId, bool _isPaused) external onlyRole(STATE_MANAGER_ROLE) { + isClaimRewardPaused[_tokenId] = _isPaused; + emit ClaimRewardPausedUpdated(_tokenId, _isPaused); + } + + function increaseCurrentSupply(uint256 _tokenId, uint256 _amount) external onlyRole(STATE_MANAGER_ROLE) { + currentRewardSupply[_tokenId] += _amount; + } + + function decreaseCurrentSupply(uint256 _tokenId, uint256 _amount) external onlyRole(STATE_MANAGER_ROLE) { + if (currentRewardSupply[_tokenId] >= _amount) { + currentRewardSupply[_tokenId] -= _amount; + } + } + + function setUserNonce(address _user, uint256 _nonce, bool _used) external onlyRole(STATE_MANAGER_ROLE) { + userNonces[_user][_nonce] = _used; + if (_used) { + emit UserNonceUsed(_user, _nonce); + } + } + + function incrementERC721RewardIndex(uint256 _rewardTokenId, uint256 _rewardIndex) external onlyRole(STATE_MANAGER_ROLE) { + erc721RewardCurrentIndex[_rewardTokenId][_rewardIndex]++; + } + + function getERC721RewardCurrentIndex(uint256 _rewardTokenId, uint256 _rewardIndex) external view returns (uint256) { + return erc721RewardCurrentIndex[_rewardTokenId][_rewardIndex]; + } + + function getAllItemIds() external view returns (uint256[] memory) { + return itemIds; + } + + function getRewardToken(uint256 _tokenId) external view returns (LibItems.RewardToken memory) { + return tokenRewards[_tokenId]; + } + + function isTokenExists(uint256 _tokenId) external view returns (bool) { + return tokenExists[_tokenId]; + } +} diff --git a/contracts/upgradeables/soulbounds/Treasury.sol b/contracts/upgradeables/soulbounds/Treasury.sol new file mode 100644 index 0000000..4fb1ae5 --- /dev/null +++ b/contracts/upgradeables/soulbounds/Treasury.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +// @author Summon.xyz Team - https://summon.xyz +// @contributors: [ @ogarciarevett, @karacurt] + +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import { ERC721HolderUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; +import { ERC1155HolderUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol"; +import { LibItems } from "../../libraries/LibItems.sol"; +import { RewardsState } from "./RewardsState.sol"; + +interface IRewards { + function getAllItemIds() external view returns (uint256[] memory); + function getTokenRewards(uint256 tokenId) external view returns (LibItems.Reward[] memory); +} + +/** + * @title Treasury + * @notice Treasury contract for managing token deposits, withdrawals, and whitelisting + * @dev This contract handles treasury business logic and delegates state management + * to RewardsState contract. Only the Rewards contract can call management functions. + * This contract is upgradeable using the UUPS pattern. + */ +contract Treasury is Initializable, AccessControlUpgradeable, UUPSUpgradeable, ERC721HolderUpgradeable, ERC1155HolderUpgradeable { + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + error AddressIsZero(); + error InvalidAmount(); + error TokenNotWhitelisted(); + error InsufficientTreasuryBalance(); + error TokenHasReserves(); + error InsufficientBalance(); + + /*////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + bytes32 public constant REWARDS_MANAGER_ROLE = keccak256("REWARDS_MANAGER_ROLE"); + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + + /*////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + RewardsState public rewardsState; + + uint256[50] private __gap; + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + event TreasuryDeposit(address indexed token, uint256 amount); + + /*////////////////////////////////////////////////////////////// + INITIALIZER + //////////////////////////////////////////////////////////////*/ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address _admin, address _rewardsContract, address _rewardsState) external initializer { + if (_admin == address(0) || _rewardsContract == address(0) || _rewardsState == address(0)) { + revert AddressIsZero(); + } + + __AccessControl_init(); + __ERC721Holder_init(); + __ERC1155Holder_init(); + + rewardsState = RewardsState(_rewardsState); + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(UPGRADER_ROLE, _admin); + _grantRole(REWARDS_MANAGER_ROLE, _rewardsContract); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} + + /*////////////////////////////////////////////////////////////// + TREASURY MANAGEMENT FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function whitelistToken(address _token, LibItems.RewardType _type) external onlyRole(REWARDS_MANAGER_ROLE) { + rewardsState.whitelistToken(_token, _type); + } + + function removeTokenFromWhitelist(address _token) external onlyRole(REWARDS_MANAGER_ROLE) { + LibItems.RewardType _type = rewardsState.tokenTypes(_token); + + if (_type == LibItems.RewardType.ERC20 && rewardsState.reservedAmounts(_token) > 0) revert TokenHasReserves(); + if (_type == LibItems.RewardType.ERC721 && rewardsState.erc721TotalReserved(_token) > 0) revert TokenHasReserves(); + if (_type == LibItems.RewardType.ERC1155 && rewardsState.erc1155TotalReserved(_token) > 0) revert TokenHasReserves(); + + rewardsState.removeTokenFromWhitelist(_token); + } + + function depositToTreasury(address _token, uint256 _amount, address _from) external onlyRole(REWARDS_MANAGER_ROLE) { + if (!rewardsState.whitelistedTokens(_token)) revert TokenNotWhitelisted(); + if (_amount == 0) revert InvalidAmount(); + + SafeERC20.safeTransferFrom(IERC20(_token), _from, address(this), _amount); + emit TreasuryDeposit(_token, _amount); + } + + function withdrawUnreservedTreasury(address _token, address _to) external onlyRole(REWARDS_MANAGER_ROLE) { + if (_to == address(0)) revert AddressIsZero(); + if (!rewardsState.whitelistedTokens(_token)) revert TokenNotWhitelisted(); + + uint256 balance = IERC20(_token).balanceOf(address(this)); + uint256 reserved = rewardsState.reservedAmounts(_token); + + if (balance <= reserved) revert InsufficientBalance(); + + SafeERC20.safeTransfer(IERC20(_token), _to, balance - reserved); + } + + function withdrawERC721UnreservedTreasury(address _token, address _to, uint256 _tokenId) external onlyRole(REWARDS_MANAGER_ROLE) { + if (_to == address(0)) revert AddressIsZero(); + if (!rewardsState.whitelistedTokens(_token)) revert TokenNotWhitelisted(); + if (rewardsState.isErc721Reserved(_token, _tokenId)) revert InsufficientTreasuryBalance(); + + IERC721(_token).safeTransferFrom(address(this), _to, _tokenId); + } + + function withdrawERC1155UnreservedTreasury(address _token, address _to, uint256 _tokenId, uint256 _amount) external onlyRole(REWARDS_MANAGER_ROLE) { + if (_to == address(0)) revert AddressIsZero(); + if (!rewardsState.whitelistedTokens(_token)) revert TokenNotWhitelisted(); + + uint256 balance = IERC1155(_token).balanceOf(address(this), _tokenId); + uint256 reserved = rewardsState.erc1155ReservedAmounts(_token, _tokenId); + + if (balance <= reserved) revert InsufficientBalance(); + if (_amount > (balance - reserved)) revert InsufficientBalance(); + + IERC1155(_token).safeTransferFrom(address(this), _to, _tokenId, _amount, ""); + } + + /*////////////////////////////////////////////////////////////// + TREASURY VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function getAllTreasuryBalances(address rewardsContract) + external + view + returns ( + address[] memory addresses, + uint256[] memory totalBalances, + uint256[] memory reservedBalances, + uint256[] memory availableBalances, + string[] memory symbols, + string[] memory names, + string[] memory types + ) + { + address[] memory whitelistedTokensArray = rewardsState.getWhitelistedTokens(); + + uint256 erc20AndErc721Count = 0; + for (uint256 i = 0; i < whitelistedTokensArray.length; i++) { + LibItems.RewardType tokenType = rewardsState.tokenTypes(whitelistedTokensArray[i]); + if (tokenType == LibItems.RewardType.ERC20 || tokenType == LibItems.RewardType.ERC721) { + erc20AndErc721Count++; + } + } + + uint256 erc1155Count = _countUniqueErc1155TokenIds(rewardsContract); + uint256 totalCount = erc20AndErc721Count + erc1155Count; + + addresses = new address[](totalCount); + totalBalances = new uint256[](totalCount); + reservedBalances = new uint256[](totalCount); + availableBalances = new uint256[](totalCount); + symbols = new string[](totalCount); + names = new string[](totalCount); + types = new string[](totalCount); + + uint256 currentIndex = 0; + + for (uint256 i = 0; i < whitelistedTokensArray.length; i++) { + address tokenAddress = whitelistedTokensArray[i]; + LibItems.RewardType tokenType = rewardsState.tokenTypes(tokenAddress); + + addresses[currentIndex] = tokenAddress; + + if (tokenType == LibItems.RewardType.ERC20) { + _processERC20Token(rewardsContract, tokenAddress, currentIndex, totalBalances, reservedBalances, availableBalances, symbols, names, types); + currentIndex++; + } else if (tokenType == LibItems.RewardType.ERC721) { + _processERC721Token(rewardsContract, tokenAddress, currentIndex, totalBalances, reservedBalances, availableBalances, symbols, names, types); + currentIndex++; + } + } + + currentIndex = _processERC1155Tokens(rewardsContract, erc1155Count, currentIndex, addresses, totalBalances, reservedBalances, availableBalances, symbols, names, types); + + return (addresses, totalBalances, reservedBalances, availableBalances, symbols, names, types); + } + + function getTreasuryBalance(address rewardsContract, address _token) external view returns (uint256) { + return IERC20(_token).balanceOf(rewardsContract); + } + + function getReservedAmount(address, address _token) external view returns (uint256) { + return rewardsState.reservedAmounts(_token); + } + + function getAvailableTreasuryBalance(address rewardsContract, address _token) external view returns (uint256) { + uint256 balance = IERC20(_token).balanceOf(rewardsContract); + uint256 reserved = rewardsState.reservedAmounts(_token); + return balance > reserved ? balance - reserved : 0; + } + + function getWhitelistedTokens(address) external view returns (address[] memory) { + return rewardsState.getWhitelistedTokens(); + } + + function isWhitelistedToken(address, address _token) external view returns (bool) { + return rewardsState.whitelistedTokens(_token); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HELPER FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _processERC20Token( + address rewardsContract, + address tokenAddress, + uint256 index, + uint256[] memory totalBalances, + uint256[] memory reservedBalances, + uint256[] memory availableBalances, + string[] memory symbols, + string[] memory names, + string[] memory types + ) private view { + uint256 totalBalance = IERC20(tokenAddress).balanceOf(rewardsContract); + uint256 reserved = rewardsState.reservedAmounts(tokenAddress); + + totalBalances[index] = totalBalance; + reservedBalances[index] = reserved; + availableBalances[index] = totalBalance > reserved ? totalBalance - reserved : 0; + + try IERC20Metadata(tokenAddress).symbol() returns (string memory symbol) { + symbols[index] = symbol; + } catch { + symbols[index] = "UNKNOWN"; + } + + try IERC20Metadata(tokenAddress).name() returns (string memory name) { + names[index] = name; + } catch { + names[index] = "Unknown Token"; + } + + types[index] = "fa"; + } + + function _processERC721Token( + address rewardsContract, + address tokenAddress, + uint256 index, + uint256[] memory totalBalances, + uint256[] memory reservedBalances, + uint256[] memory availableBalances, + string[] memory symbols, + string[] memory names, + string[] memory types + ) private view { + uint256 totalBalance = IERC721(tokenAddress).balanceOf(rewardsContract); + uint256 reserved = rewardsState.erc721TotalReserved(tokenAddress); + + totalBalances[index] = totalBalance; + reservedBalances[index] = reserved; + availableBalances[index] = totalBalance > reserved ? totalBalance - reserved : 0; + + try IERC721Metadata(tokenAddress).symbol() returns (string memory symbol) { + symbols[index] = symbol; + } catch { + symbols[index] = "ERC721"; + } + + try IERC721Metadata(tokenAddress).name() returns (string memory name) { + names[index] = name; + } catch { + names[index] = "NFT Collection"; + } + + types[index] = "nft"; + } + + function _processERC1155Tokens( + address rewardsContract, + uint256 erc1155Count, + uint256 startIndex, + address[] memory addresses, + uint256[] memory totalBalances, + uint256[] memory reservedBalances, + uint256[] memory availableBalances, + string[] memory symbols, + string[] memory names, + string[] memory types + ) private view returns (uint256) { + IRewards rewards = IRewards(rewardsContract); + uint256[] memory itemIds = rewards.getAllItemIds(); + + address[] memory processedErc1155Addresses = new address[](erc1155Count); + uint256[] memory processedErc1155TokenIds = new uint256[](erc1155Count); + uint256 processedCount = 0; + uint256 currentIndex = startIndex; + + for (uint256 i = 0; i < itemIds.length; i++) { + LibItems.Reward[] memory tokenRewards = rewards.getTokenRewards(itemIds[i]); + + for (uint256 j = 0; j < tokenRewards.length; j++) { + LibItems.Reward memory reward = tokenRewards[j]; + + if (reward.rewardType != LibItems.RewardType.ERC1155) continue; + + address erc1155Address = reward.rewardTokenAddress; + uint256 erc1155TokenId = reward.rewardTokenId; + + bool alreadyAdded = false; + for (uint256 k = 0; k < processedCount; k++) { + if (processedErc1155Addresses[k] == erc1155Address && + processedErc1155TokenIds[k] == erc1155TokenId) { + alreadyAdded = true; + break; + } + } + + if (!alreadyAdded && currentIndex < addresses.length) { + processedErc1155Addresses[processedCount] = erc1155Address; + processedErc1155TokenIds[processedCount] = erc1155TokenId; + processedCount++; + + addresses[currentIndex] = erc1155Address; + + uint256 balance = IERC1155(erc1155Address).balanceOf(rewardsContract, erc1155TokenId); + uint256 reserved = rewardsState.erc1155ReservedAmounts(erc1155Address, erc1155TokenId); + + totalBalances[currentIndex] = balance; + reservedBalances[currentIndex] = reserved; + availableBalances[currentIndex] = balance > reserved ? balance - reserved : 0; + + names[currentIndex] = "ERC1155 Collection"; + symbols[currentIndex] = "ERC1155"; + types[currentIndex] = "nft"; + + currentIndex++; + } + } + } + + return currentIndex; + } + + function _countUniqueErc1155TokenIds(address rewardsContract) private view returns (uint256) { + IRewards rewards = IRewards(rewardsContract); + uint256[] memory itemIds = rewards.getAllItemIds(); + + address[] memory uniqueAddresses = new address[](itemIds.length * 10); + uint256[] memory uniqueTokenIds = new uint256[](itemIds.length * 10); + uint256 count = 0; + + for (uint256 i = 0; i < itemIds.length; i++) { + LibItems.Reward[] memory tokenRewards = rewards.getTokenRewards(itemIds[i]); + + for (uint256 j = 0; j < tokenRewards.length; j++) { + if (tokenRewards[j].rewardType == LibItems.RewardType.ERC1155) { + address erc1155Address = tokenRewards[j].rewardTokenAddress; + uint256 erc1155TokenId = tokenRewards[j].rewardTokenId; + + bool found = false; + for (uint256 k = 0; k < count; k++) { + if (uniqueAddresses[k] == erc1155Address && + uniqueTokenIds[k] == erc1155TokenId) { + found = true; + break; + } + } + + if (!found) { + uniqueAddresses[count] = erc1155Address; + uniqueTokenIds[count] = erc1155TokenId; + count++; + } + } + } + } + + return count; + } + + function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControlUpgradeable, ERC1155HolderUpgradeable) returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index 579f25c..3a05720 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -29,7 +29,7 @@ const config: HardhatUserConfig = { settings: { optimizer: { enabled: true, - runs: 200, + runs: 5, // Minimal runs for maximum size reduction details: { yul: true, }, diff --git a/scripts/upgradeRewards.ts b/scripts/upgradeRewards.ts new file mode 100644 index 0000000..2915917 --- /dev/null +++ b/scripts/upgradeRewards.ts @@ -0,0 +1,96 @@ +import { ethers, upgrades } from 'hardhat'; + +async function main() { + const [deployer] = await ethers.getSigners(); + console.log('Upgrading Rewards contract with account:', deployer.address); + console.log('Account balance:', (await ethers.provider.getBalance(deployer.address)).toString()); + + // The address of the deployed proxy (you need to provide this) + // Replace with your actual proxy address + const PROXY_ADDRESS = process.env.REWARDS_PROXY_ADDRESS || '0x39aA1cBfabFd26D616C22bcC70964776CEFD2DAf'; + + if (!PROXY_ADDRESS) { + console.error('Error: Please provide REWARDS_PROXY_ADDRESS environment variable'); + console.log('Usage: REWARDS_PROXY_ADDRESS=0x... npx hardhat run scripts/upgradeRewards.ts --network '); + process.exit(1); + } + + console.log('\nUpgrading Rewards proxy at:', PROXY_ADDRESS); + + // Get the new implementation + const RewardsV2 = await ethers.getContractFactory('Rewards'); + + // Force import the proxy if it's not registered (needed for previously deployed proxies) + console.log('Registering existing proxy...'); + try { + await upgrades.forceImport(PROXY_ADDRESS, RewardsV2); + console.log('Proxy registered successfully'); + } catch (error: any) { + console.log('Proxy already registered or error:', error.message); + } + + // Upgrade the proxy + console.log('Preparing upgrade...'); + const upgraded = await upgrades.upgradeProxy(PROXY_ADDRESS, RewardsV2); + await upgraded.waitForDeployment(); + + const upgradedAddress = await upgraded.getAddress(); + console.log('Rewards proxy upgraded successfully!'); + console.log('Proxy address (unchanged):', upgradedAddress); + + // Get the implementation address + const implementationAddress = await upgrades.erc1967.getImplementationAddress(PROXY_ADDRESS); + console.log('New implementation address:', implementationAddress); + + console.log('\n========================================'); + console.log('Upgrade Summary:'); + console.log('========================================'); + console.log('Proxy Address:', PROXY_ADDRESS); + console.log('New Implementation:', implementationAddress); + console.log('Upgraded by:', deployer.address); + console.log('========================================'); + + // Test treasury delegation + console.log('\nTesting Treasury contract delegation...'); + const treasuryAddress = await upgraded.treasury(); + console.log('Treasury contract address:', treasuryAddress); + + if (treasuryAddress && treasuryAddress !== '0x0000000000000000000000000000000000000000') { + try { + const result = await upgraded.getAllTreasuryBalances(); + console.log('getAllTreasuryBalances call successful!'); + console.log('Number of tokens in treasury:', result.addresses.length); + + if (result.addresses.length > 0) { + console.log('\nTreasury tokens:'); + for (let i = 0; i < result.addresses.length; i++) { + console.log(` ${i + 1}. ${result.addresses[i]}`); + console.log(` Type: ${result.types[i]}`); + console.log(` Symbol: ${result.symbols[i]}`); + console.log(` Name: ${result.names[i]}`); + console.log(` Total Balance: ${result.totalBalances[i]}`); + console.log(` Reserved: ${result.reservedBalances[i]}`); + console.log(` Available: ${result.availableBalances[i]}`); + console.log(''); + } + } else { + console.log('No tokens in treasury yet.'); + } + } catch (error: any) { + console.error('Error calling getAllTreasuryBalances:', error.message); + } + } else { + console.log('Treasury contract not set. Call setTreasury() after deploying Treasury contract.'); + } + + // Verify instructions + console.log('\nTo verify the new implementation on Etherscan:'); + console.log(`npx hardhat verify --network ${implementationAddress}`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); \ No newline at end of file From f7322e6e87026c1e8331e3063b801300ae0705cb Mon Sep 17 00:00:00 2001 From: ogarciarevett Date: Thu, 5 Feb 2026 15:29:54 +0100 Subject: [PATCH 2/4] feat: split more the rewards contract(UUPS) --- contracts/upgradeables/soulbounds/Rewards.sol | 56 +--- hardhat.config.ts | 2 +- scripts/deployKpopBadges.ts | 80 ------ scripts/deployRewards.ts | 93 ------- scripts/deployRewardsSystemSepolia.ts | 246 ++++++++++++++++++ scripts/upgradeRewards.ts | 96 ------- scripts/upgradeRewardsSystem.ts | 199 ++++++++++++++ 7 files changed, 447 insertions(+), 325 deletions(-) delete mode 100644 scripts/deployKpopBadges.ts delete mode 100644 scripts/deployRewards.ts create mode 100644 scripts/deployRewardsSystemSepolia.ts delete mode 100644 scripts/upgradeRewards.ts create mode 100644 scripts/upgradeRewardsSystem.ts diff --git a/contracts/upgradeables/soulbounds/Rewards.sol b/contracts/upgradeables/soulbounds/Rewards.sol index 2d2672b..b020b37 100644 --- a/contracts/upgradeables/soulbounds/Rewards.sol +++ b/contracts/upgradeables/soulbounds/Rewards.sol @@ -211,17 +211,6 @@ contract Rewards is return _state().getRewardToken(_tokenId).rewards; } - function decodeData( - bytes calldata _data - ) - external - view - onlyRole(DEV_CONFIG_ROLE) - returns (address, uint256, uint256, uint256[] memory) - { - return _decodeData(_data); - } - function _decodeData( bytes calldata _data ) private pure returns (address, uint256, uint256, uint256[] memory) { @@ -884,7 +873,7 @@ contract Rewards is function _verifyContractChainIdAndDecode( bytes calldata data ) private view returns (uint256[] memory) { - uint256 currentChainId = getChainID(); + uint256 currentChainId = block.chainid; ( address contractAddress, uint256 chainId, @@ -975,32 +964,6 @@ contract Rewards is return rewardTokenContract.balanceOf(_user, _tokenId) > 0; } - /** - * @dev Get the NFT distribution progress for a reward token. - * @param _tokenId The ID of the reward token. - * @param _rewardIndex The index of the reward in the rewards array. - * @return distributed The number of NFTs already distributed. - * @return total The total number of NFTs for this reward. - */ - function getNftDistributionProgress( - uint256 _tokenId, - uint256 _rewardIndex - ) external view returns (uint256 distributed, uint256 total) { - if (!isTokenExist(_tokenId)) { - return (0, 0); - } - LibItems.RewardToken memory rewardToken = _state().getRewardToken(_tokenId); - if (_rewardIndex >= rewardToken.rewards.length) { - return (0, 0); - } - LibItems.Reward memory reward = rewardToken.rewards[_rewardIndex]; - if (reward.rewardType != LibItems.RewardType.ERC721) { - return (0, 0); - } - distributed = _state().getERC721RewardCurrentIndex(_tokenId, _rewardIndex); - total = reward.rewardTokenIds.length; - } - /** * @dev Get the remaining supply for a reward token. * @param _tokenId The ID of the reward token. @@ -1135,15 +1098,6 @@ contract Rewards is // Fallback function is called when msg.data is not empty fallback() external payable {} - function adminVerifySignature( - address to, - uint256 nonce, - bytes calldata data, - bytes calldata signature - ) public onlyRole(DEV_CONFIG_ROLE) returns (bool) { - return _verifySignature(to, nonce, data, signature); - } - function addWhitelistSigner( address _signer ) external onlyRole(DEV_CONFIG_ROLE) { @@ -1155,12 +1109,4 @@ contract Rewards is ) external onlyRole(DEV_CONFIG_ROLE) { _removeWhitelistSigner(signer); } - - function getChainID() public view returns (uint256) { - uint256 id; - assembly { - id := chainid() - } - return id; - } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 3a05720..d9c728a 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -29,7 +29,7 @@ const config: HardhatUserConfig = { settings: { optimizer: { enabled: true, - runs: 5, // Minimal runs for maximum size reduction + runs: 1, // Minimal runs for maximum size reduction details: { yul: true, }, diff --git a/scripts/deployKpopBadges.ts b/scripts/deployKpopBadges.ts deleted file mode 100644 index 86b6846..0000000 --- a/scripts/deployKpopBadges.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { ethers } from 'hardhat'; - -async function main() { - const [deployer] = await ethers.getSigners(); - console.log('Deploying KPOP Badges with account:', deployer.address); - console.log('Account balance:', (await ethers.provider.getBalance(deployer.address)).toString()); - - // Configuration - const name = 'KPOP Badges'; - const symbol = 'KPOP'; - const baseURI = 'https://summon.xyz/kpop/badges/'; - const contractURI = 'https://summon.xyz/kpop/contract/'; - const maxPerMint = 100; - const isPaused = false; - const devWallet = deployer.address; - - // Deploy ERC1155Soulbound - console.log('\n1. Deploying ERC1155Soulbound (KPOP Badges)...'); - const ERC1155Soulbound = await ethers.getContractFactory('ERC1155Soulbound'); - const kpopBadges = await ERC1155Soulbound.deploy( - name, - symbol, - baseURI, - contractURI, - maxPerMint, - isPaused, - devWallet - ); - await kpopBadges.waitForDeployment(); - const kpopBadgesAddress = await kpopBadges.getAddress(); - console.log('KPOP Badges deployed to:', kpopBadgesAddress); - - // Add some initial badge tokens - console.log('\n2. Adding initial badge tokens...'); - const badgeTokens = [ - { tokenId: 1, tokenUri: 'https://summon.xyz/kpop/badges/1/metadata.json', receiver: ethers.ZeroAddress, feeBasisPoints: 0 }, - { tokenId: 2, tokenUri: 'https://summon.xyz/kpop/badges/2/metadata.json', receiver: ethers.ZeroAddress, feeBasisPoints: 0 }, - { tokenId: 3, tokenUri: 'https://summon.xyz/kpop/badges/3/metadata.json', receiver: ethers.ZeroAddress, feeBasisPoints: 0 }, - ]; - - for (const token of badgeTokens) { - const tx = await kpopBadges.addNewToken(token); - await tx.wait(); - console.log(` Badge #${token.tokenId} added`); - } - - // Whitelist the Rewards contract if provided - const REWARDS_CONTRACT = process.env.REWARDS_CONTRACT || '0x073e96B3Df99e6fBA615d9B3d2d7DF83cb005b41'; - if (REWARDS_CONTRACT && REWARDS_CONTRACT !== ethers.ZeroAddress) { - console.log('\n3. Whitelisting Rewards contract for soulbound transfers...'); - const whitelistTx = await kpopBadges.updateWhitelistAddress(REWARDS_CONTRACT, true); - await whitelistTx.wait(); - console.log(' Rewards contract whitelisted:', REWARDS_CONTRACT); - } - - console.log('\n========================================'); - console.log('Deployment Summary:'); - console.log('========================================'); - console.log('KPOP Badges (ERC1155Soulbound):', kpopBadgesAddress); - console.log('Dev Wallet:', devWallet); - console.log('Rewards Contract (whitelisted):', REWARDS_CONTRACT); - console.log('========================================'); - - // Verify instructions - console.log('\nTo verify contract on Etherscan:'); - console.log(`npx hardhat verify --network sepolia ${kpopBadgesAddress} "${name}" "${symbol}" "${baseURI}" "${contractURI}" ${maxPerMint} ${isPaused} ${devWallet}`); - - console.log('\nTo use with Rewards contract:'); - console.log('1. Manager mints soulbound badges to themselves'); - console.log('2. Manager approves Rewards contract'); - console.log('3. Manager creates reward token with ERC1155 badge as reward'); - console.log('4. Users claim rewards and receive non-transferable badges'); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); diff --git a/scripts/deployRewards.ts b/scripts/deployRewards.ts deleted file mode 100644 index 24ff689..0000000 --- a/scripts/deployRewards.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { ethers } from 'hardhat'; - -async function main() { - const [deployer] = await ethers.getSigners(); - console.log('Deploying contracts with account:', deployer.address); - console.log('Account balance:', (await ethers.provider.getBalance(deployer.address)).toString()); - - // Configuration - using deployer as all roles for testing - const devWallet = deployer.address; - const managerWallet = deployer.address; - const minterWallet = deployer.address; - - // 1. Deploy AccessToken - console.log('\n1. Deploying AccessToken...'); - const AccessToken = await ethers.getContractFactory('AccessToken'); - const accessToken = await AccessToken.deploy(devWallet); - await accessToken.waitForDeployment(); - const accessTokenAddress = await accessToken.getAddress(); - console.log('AccessToken deployed to:', accessTokenAddress); - - // 2. Initialize AccessToken - console.log('\n2. Initializing AccessToken...'); - const initAccessTokenTx = await accessToken.initialize( - 'Rewards Access Token', // name - 'RAT', // symbol - 'https://summon.xyz/metadata/', // defaultTokenURI - 'https://summon.xyz/contract/', // contractURI - devWallet, - devWallet // minterContract - will be updated to Rewards contract - ); - await initAccessTokenTx.wait(); - console.log('AccessToken initialized'); - - // 3. Deploy Rewards - console.log('\n3. Deploying Rewards...'); - const Rewards = await ethers.getContractFactory('Rewards'); - const rewards = await Rewards.deploy(devWallet); - await rewards.waitForDeployment(); - const rewardsAddress = await rewards.getAddress(); - console.log('Rewards deployed to:', rewardsAddress); - - // 4. Initialize Rewards - console.log('\n4. Initializing Rewards...'); - const initRewardsTx = await rewards.initialize( - devWallet, - managerWallet, - minterWallet, - accessTokenAddress - ); - await initRewardsTx.wait(); - console.log('Rewards initialized'); - - // 5. Grant MINTER_ROLE to Rewards contract on AccessToken - console.log('\n5. Granting MINTER_ROLE to Rewards on AccessToken...'); - const MINTER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('MINTER_ROLE')); - const grantRoleTx = await accessToken.grantRole(MINTER_ROLE, rewardsAddress); - await grantRoleTx.wait(); - console.log('MINTER_ROLE granted'); - - // 6. Add Rewards to whitelist on AccessToken (for whitelistBurn) - console.log('\n6. Adding Rewards to whitelist on AccessToken...'); - const DEV_CONFIG_ROLE = ethers.keccak256(ethers.toUtf8Bytes('DEV_CONFIG_ROLE')); - // Check if addToWhitelist function exists - try { - const addWhitelistTx = await accessToken.addToWhitelist(rewardsAddress); - await addWhitelistTx.wait(); - console.log('Rewards added to whitelist'); - } catch (error) { - console.log('Note: addToWhitelist may need to be called separately or function name differs'); - } - - console.log('\n========================================'); - console.log('Deployment Summary:'); - console.log('========================================'); - console.log('AccessToken:', accessTokenAddress); - console.log('Rewards:', rewardsAddress); - console.log('Dev Wallet:', devWallet); - console.log('Manager Wallet:', managerWallet); - console.log('Minter Wallet:', minterWallet); - console.log('========================================'); - - // Verify instructions - console.log('\nTo verify contracts on Etherscan:'); - console.log(`npx hardhat verify --network sepolia ${accessTokenAddress} ${devWallet}`); - console.log(`npx hardhat verify --network sepolia ${rewardsAddress} ${devWallet}`); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); diff --git a/scripts/deployRewardsSystemSepolia.ts b/scripts/deployRewardsSystemSepolia.ts new file mode 100644 index 0000000..cbe07f7 --- /dev/null +++ b/scripts/deployRewardsSystemSepolia.ts @@ -0,0 +1,246 @@ +import { ethers, upgrades, run } from 'hardhat'; + +/** + * Full deployment script for the Rewards system on Sepolia + * + * Deploys: + * 1. AccessToken (standard deployment) + * 2. RewardsState (UUPS proxy) + * 3. Rewards (UUPS proxy) + * 4. Treasury (UUPS proxy) + * + * Then sets up all relationships between contracts. + * + * Usage: + * pnpm hardhat run scripts/deployRewardsSystemSepolia.ts --network sepolia + */ + +async function main() { + const [deployer] = await ethers.getSigners(); + + console.log('========================================'); + console.log('Rewards System Deployment - Sepolia'); + console.log('========================================'); + console.log('Deployer:', deployer.address); + console.log('Balance:', ethers.formatEther(await ethers.provider.getBalance(deployer.address)), 'ETH'); + console.log('========================================\n'); + + // Configuration - using deployer for all roles initially + const devWallet = deployer.address; + const managerWallet = deployer.address; + const minterWallet = deployer.address; + + // ======================================== + // 1. Deploy AccessToken (Standard) + // ======================================== + console.log('1. Deploying AccessToken...'); + const AccessToken = await ethers.getContractFactory('AccessToken'); + const accessToken = await AccessToken.deploy(devWallet); + await accessToken.waitForDeployment(); + const accessTokenAddress = await accessToken.getAddress(); + console.log(' AccessToken deployed to:', accessTokenAddress); + + // ======================================== + // 2. Deploy RewardsState (UUPS Proxy) + // ======================================== + console.log('\n2. Deploying RewardsState (UUPS Proxy)...'); + const RewardsState = await ethers.getContractFactory('RewardsState'); + const rewardsState = await upgrades.deployProxy( + RewardsState, + [devWallet], // initialize(address _admin) + { kind: 'uups', initializer: 'initialize' } + ); + await rewardsState.waitForDeployment(); + const rewardsStateAddress = await rewardsState.getAddress(); + const rewardsStateImpl = await upgrades.erc1967.getImplementationAddress(rewardsStateAddress); + console.log(' RewardsState Proxy:', rewardsStateAddress); + console.log(' RewardsState Implementation:', rewardsStateImpl); + + // ======================================== + // 3. Deploy Rewards (UUPS Proxy) + // ======================================== + console.log('\n3. Deploying Rewards (UUPS Proxy)...'); + const Rewards = await ethers.getContractFactory('Rewards'); + const rewards = await upgrades.deployProxy( + Rewards, + [devWallet, managerWallet, minterWallet, accessTokenAddress], + { kind: 'uups', initializer: 'initialize' } + ); + await rewards.waitForDeployment(); + const rewardsAddress = await rewards.getAddress(); + const rewardsImpl = await upgrades.erc1967.getImplementationAddress(rewardsAddress); + console.log(' Rewards Proxy:', rewardsAddress); + console.log(' Rewards Implementation:', rewardsImpl); + + // ======================================== + // 4. Deploy Treasury (UUPS Proxy) + // ======================================== + console.log('\n4. Deploying Treasury (UUPS Proxy)...'); + const Treasury = await ethers.getContractFactory('Treasury'); + const treasury = await upgrades.deployProxy( + Treasury, + [devWallet, rewardsAddress, rewardsStateAddress], // initialize(admin, rewardsContract, rewardsState) + { kind: 'uups', initializer: 'initialize' } + ); + await treasury.waitForDeployment(); + const treasuryAddress = await treasury.getAddress(); + const treasuryImpl = await upgrades.erc1967.getImplementationAddress(treasuryAddress); + console.log(' Treasury Proxy:', treasuryAddress); + console.log(' Treasury Implementation:', treasuryImpl); + + // ======================================== + // 5. Configure relationships + // ======================================== + console.log('\n5. Configuring contract relationships...'); + + // 5a. Set Treasury and RewardsState on Rewards contract + console.log(' Setting Treasury on Rewards...'); + const setTreasuryTx = await rewards.setTreasury(treasuryAddress); + await setTreasuryTx.wait(); + + console.log(' Setting RewardsState on Rewards...'); + const setStateTx = await rewards.setRewardsState(rewardsStateAddress); + await setStateTx.wait(); + + // 5b. Grant STATE_MANAGER_ROLE to Rewards and Treasury on RewardsState + const STATE_MANAGER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('STATE_MANAGER_ROLE')); + + console.log(' Granting STATE_MANAGER_ROLE to Rewards on RewardsState...'); + const grantRewardsTx = await rewardsState.grantRole(STATE_MANAGER_ROLE, rewardsAddress); + await grantRewardsTx.wait(); + + console.log(' Granting STATE_MANAGER_ROLE to Treasury on RewardsState...'); + const grantTreasuryTx = await rewardsState.grantRole(STATE_MANAGER_ROLE, treasuryAddress); + await grantTreasuryTx.wait(); + + // ======================================== + // 6. Initialize AccessToken + // ======================================== + console.log('\n6. Initializing AccessToken...'); + const initAccessTokenTx = await accessToken.initialize( + 'Rewards Access Token', + 'RAT', + 'https://summon.xyz/metadata/', + 'https://summon.xyz/contract/', + devWallet, + rewardsAddress // minterContract is the Rewards contract + ); + await initAccessTokenTx.wait(); + console.log(' AccessToken initialized'); + + // 6b. Grant MINTER_ROLE to Rewards on AccessToken + const MINTER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('MINTER_ROLE')); + console.log(' Granting MINTER_ROLE to Rewards on AccessToken...'); + const grantMinterTx = await accessToken.grantRole(MINTER_ROLE, rewardsAddress); + await grantMinterTx.wait(); + + // ======================================== + // 7. Verify contracts on Etherscan + // ======================================== + console.log('\n7. Verifying contracts on Etherscan...'); + console.log(' (Waiting for block confirmations...)'); + + // Wait for confirmations + await new Promise((resolve) => setTimeout(resolve, 30000)); + + // Verify AccessToken + try { + console.log(' Verifying AccessToken...'); + await run('verify:verify', { + address: accessTokenAddress, + constructorArguments: [devWallet], + }); + console.log(' AccessToken verified!'); + } catch (e: any) { + console.log(' AccessToken verification:', e.message.includes('Already') ? 'Already verified' : e.message); + } + + // Verify RewardsState implementation + try { + console.log(' Verifying RewardsState implementation...'); + await run('verify:verify', { + address: rewardsStateImpl, + constructorArguments: [], + }); + console.log(' RewardsState verified!'); + } catch (e: any) { + console.log(' RewardsState verification:', e.message.includes('Already') ? 'Already verified' : e.message); + } + + // Verify Rewards implementation + try { + console.log(' Verifying Rewards implementation...'); + await run('verify:verify', { + address: rewardsImpl, + constructorArguments: [], + }); + console.log(' Rewards verified!'); + } catch (e: any) { + console.log(' Rewards verification:', e.message.includes('Already') ? 'Already verified' : e.message); + } + + // Verify Treasury implementation + try { + console.log(' Verifying Treasury implementation...'); + await run('verify:verify', { + address: treasuryImpl, + constructorArguments: [], + }); + console.log(' Treasury verified!'); + } catch (e: any) { + console.log(' Treasury verification:', e.message.includes('Already') ? 'Already verified' : e.message); + } + + // ======================================== + // Summary + // ======================================== + console.log('\n========================================'); + console.log('DEPLOYMENT SUMMARY'); + console.log('========================================'); + console.log('Network: Sepolia'); + console.log('Deployer:', deployer.address); + console.log(''); + console.log('Contracts:'); + console.log(' AccessToken:'); + console.log(' Address:', accessTokenAddress); + console.log(''); + console.log(' RewardsState (UUPS):'); + console.log(' Proxy:', rewardsStateAddress); + console.log(' Implementation:', rewardsStateImpl); + console.log(''); + console.log(' Rewards (UUPS):'); + console.log(' Proxy:', rewardsAddress); + console.log(' Implementation:', rewardsImpl); + console.log(''); + console.log(' Treasury (UUPS):'); + console.log(' Proxy:', treasuryAddress); + console.log(' Implementation:', treasuryImpl); + console.log(''); + console.log('Roles configured:'); + console.log(' - Rewards has STATE_MANAGER_ROLE on RewardsState'); + console.log(' - Treasury has STATE_MANAGER_ROLE on RewardsState'); + console.log(' - Rewards has MINTER_ROLE on AccessToken'); + console.log('========================================'); + + console.log('\nEtherscan URLs:'); + console.log(` AccessToken: https://sepolia.etherscan.io/address/${accessTokenAddress}#code`); + console.log(` RewardsState: https://sepolia.etherscan.io/address/${rewardsStateAddress}#code`); + console.log(` Rewards: https://sepolia.etherscan.io/address/${rewardsAddress}#code`); + console.log(` Treasury: https://sepolia.etherscan.io/address/${treasuryAddress}#code`); + + // Export addresses for other scripts + console.log('\n========================================'); + console.log('ENVIRONMENT VARIABLES (copy to .env):'); + console.log('========================================'); + console.log(`ACCESS_TOKEN_ADDRESS=${accessTokenAddress}`); + console.log(`REWARDS_STATE_ADDRESS=${rewardsStateAddress}`); + console.log(`REWARDS_PROXY_ADDRESS=${rewardsAddress}`); + console.log(`TREASURY_ADDRESS=${treasuryAddress}`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error('Deployment failed:', error); + process.exit(1); + }); diff --git a/scripts/upgradeRewards.ts b/scripts/upgradeRewards.ts deleted file mode 100644 index 2915917..0000000 --- a/scripts/upgradeRewards.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ethers, upgrades } from 'hardhat'; - -async function main() { - const [deployer] = await ethers.getSigners(); - console.log('Upgrading Rewards contract with account:', deployer.address); - console.log('Account balance:', (await ethers.provider.getBalance(deployer.address)).toString()); - - // The address of the deployed proxy (you need to provide this) - // Replace with your actual proxy address - const PROXY_ADDRESS = process.env.REWARDS_PROXY_ADDRESS || '0x39aA1cBfabFd26D616C22bcC70964776CEFD2DAf'; - - if (!PROXY_ADDRESS) { - console.error('Error: Please provide REWARDS_PROXY_ADDRESS environment variable'); - console.log('Usage: REWARDS_PROXY_ADDRESS=0x... npx hardhat run scripts/upgradeRewards.ts --network '); - process.exit(1); - } - - console.log('\nUpgrading Rewards proxy at:', PROXY_ADDRESS); - - // Get the new implementation - const RewardsV2 = await ethers.getContractFactory('Rewards'); - - // Force import the proxy if it's not registered (needed for previously deployed proxies) - console.log('Registering existing proxy...'); - try { - await upgrades.forceImport(PROXY_ADDRESS, RewardsV2); - console.log('Proxy registered successfully'); - } catch (error: any) { - console.log('Proxy already registered or error:', error.message); - } - - // Upgrade the proxy - console.log('Preparing upgrade...'); - const upgraded = await upgrades.upgradeProxy(PROXY_ADDRESS, RewardsV2); - await upgraded.waitForDeployment(); - - const upgradedAddress = await upgraded.getAddress(); - console.log('Rewards proxy upgraded successfully!'); - console.log('Proxy address (unchanged):', upgradedAddress); - - // Get the implementation address - const implementationAddress = await upgrades.erc1967.getImplementationAddress(PROXY_ADDRESS); - console.log('New implementation address:', implementationAddress); - - console.log('\n========================================'); - console.log('Upgrade Summary:'); - console.log('========================================'); - console.log('Proxy Address:', PROXY_ADDRESS); - console.log('New Implementation:', implementationAddress); - console.log('Upgraded by:', deployer.address); - console.log('========================================'); - - // Test treasury delegation - console.log('\nTesting Treasury contract delegation...'); - const treasuryAddress = await upgraded.treasury(); - console.log('Treasury contract address:', treasuryAddress); - - if (treasuryAddress && treasuryAddress !== '0x0000000000000000000000000000000000000000') { - try { - const result = await upgraded.getAllTreasuryBalances(); - console.log('getAllTreasuryBalances call successful!'); - console.log('Number of tokens in treasury:', result.addresses.length); - - if (result.addresses.length > 0) { - console.log('\nTreasury tokens:'); - for (let i = 0; i < result.addresses.length; i++) { - console.log(` ${i + 1}. ${result.addresses[i]}`); - console.log(` Type: ${result.types[i]}`); - console.log(` Symbol: ${result.symbols[i]}`); - console.log(` Name: ${result.names[i]}`); - console.log(` Total Balance: ${result.totalBalances[i]}`); - console.log(` Reserved: ${result.reservedBalances[i]}`); - console.log(` Available: ${result.availableBalances[i]}`); - console.log(''); - } - } else { - console.log('No tokens in treasury yet.'); - } - } catch (error: any) { - console.error('Error calling getAllTreasuryBalances:', error.message); - } - } else { - console.log('Treasury contract not set. Call setTreasury() after deploying Treasury contract.'); - } - - // Verify instructions - console.log('\nTo verify the new implementation on Etherscan:'); - console.log(`npx hardhat verify --network ${implementationAddress}`); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); \ No newline at end of file diff --git a/scripts/upgradeRewardsSystem.ts b/scripts/upgradeRewardsSystem.ts new file mode 100644 index 0000000..c9a69d4 --- /dev/null +++ b/scripts/upgradeRewardsSystem.ts @@ -0,0 +1,199 @@ +import { ethers, upgrades, run } from 'hardhat'; + +/** + * Upgrade script for the Rewards system UUPS contracts + * + * Can upgrade: + * - Rewards + * - RewardsState + * - Treasury + * + * Usage: + * # Upgrade all contracts + * pnpm hardhat run scripts/upgradeRewardsSystem.ts --network sepolia + * + * # Upgrade specific contract via env var + * UPGRADE_CONTRACT=Rewards pnpm hardhat run scripts/upgradeRewardsSystem.ts --network sepolia + * UPGRADE_CONTRACT=RewardsState pnpm hardhat run scripts/upgradeRewardsSystem.ts --network sepolia + * UPGRADE_CONTRACT=Treasury pnpm hardhat run scripts/upgradeRewardsSystem.ts --network sepolia + * + * Environment variables: + * REWARDS_PROXY_ADDRESS - Rewards proxy address + * REWARDS_STATE_ADDRESS - RewardsState proxy address + * TREASURY_ADDRESS - Treasury proxy address + * UPGRADE_CONTRACT - (optional) Specific contract to upgrade: "Rewards", "RewardsState", "Treasury", or "all" + */ + +// Default addresses (update after initial deployment) +const DEFAULT_ADDRESSES = { + REWARDS_PROXY: process.env.REWARDS_PROXY_ADDRESS || '', + REWARDS_STATE: process.env.REWARDS_STATE_ADDRESS || '', + TREASURY: process.env.TREASURY_ADDRESS || '', +}; + +interface UpgradeResult { + name: string; + proxyAddress: string; + oldImplementation: string; + newImplementation: string; + upgraded: boolean; +} + +async function upgradeContract( + contractName: string, + proxyAddress: string +): Promise { + console.log(`\nUpgrading ${contractName}...`); + console.log(` Proxy: ${proxyAddress}`); + + // Get current implementation + let oldImplementation: string; + try { + oldImplementation = await upgrades.erc1967.getImplementationAddress(proxyAddress); + console.log(` Current implementation: ${oldImplementation}`); + } catch { + throw new Error(`Could not get implementation for ${proxyAddress}. Is this a valid proxy?`); + } + + // Get contract factory + const ContractFactory = await ethers.getContractFactory(contractName); + + // Force import (required if not originally deployed via hardhat-upgrades) + try { + await upgrades.forceImport(proxyAddress, ContractFactory); + console.log(' Proxy registered with upgrades plugin'); + } catch (e: any) { + if (!e.message.includes('already been imported')) { + console.log(' Note:', e.message); + } + } + + // Upgrade + const upgraded = await upgrades.upgradeProxy(proxyAddress, ContractFactory, { + kind: 'uups', + }); + await upgraded.waitForDeployment(); + + // Get new implementation + const newImplementation = await upgrades.erc1967.getImplementationAddress(proxyAddress); + console.log(` New implementation: ${newImplementation}`); + + const wasUpgraded = oldImplementation.toLowerCase() !== newImplementation.toLowerCase(); + console.log(` Upgraded: ${wasUpgraded ? 'YES' : 'NO (same implementation)'}`); + + return { + name: contractName, + proxyAddress, + oldImplementation, + newImplementation, + upgraded: wasUpgraded, + }; +} + +async function verifyContract(implementationAddress: string, contractName: string) { + try { + console.log(` Verifying ${contractName} at ${implementationAddress}...`); + await run('verify:verify', { + address: implementationAddress, + constructorArguments: [], + }); + console.log(` ${contractName} verified!`); + } catch (e: any) { + if (e.message.includes('Already')) { + console.log(` ${contractName}: Already verified`); + } else { + console.log(` ${contractName} verification failed:`, e.message); + } + } +} + +async function main() { + const [deployer] = await ethers.getSigners(); + const upgradeTarget = process.env.UPGRADE_CONTRACT || 'all'; + + console.log('========================================'); + console.log('Rewards System Upgrade'); + console.log('========================================'); + console.log('Deployer:', deployer.address); + console.log('Balance:', ethers.formatEther(await ethers.provider.getBalance(deployer.address)), 'ETH'); + console.log('Target:', upgradeTarget); + console.log('========================================'); + + const results: UpgradeResult[] = []; + + // Determine which contracts to upgrade + const shouldUpgrade = { + Rewards: upgradeTarget === 'all' || upgradeTarget === 'Rewards', + RewardsState: upgradeTarget === 'all' || upgradeTarget === 'RewardsState', + Treasury: upgradeTarget === 'all' || upgradeTarget === 'Treasury', + }; + + // Upgrade Rewards + if (shouldUpgrade.Rewards) { + if (!DEFAULT_ADDRESSES.REWARDS_PROXY) { + console.log('\nSkipping Rewards: REWARDS_PROXY_ADDRESS not set'); + } else { + const result = await upgradeContract('Rewards', DEFAULT_ADDRESSES.REWARDS_PROXY); + results.push(result); + } + } + + // Upgrade RewardsState + if (shouldUpgrade.RewardsState) { + if (!DEFAULT_ADDRESSES.REWARDS_STATE) { + console.log('\nSkipping RewardsState: REWARDS_STATE_ADDRESS not set'); + } else { + const result = await upgradeContract('RewardsState', DEFAULT_ADDRESSES.REWARDS_STATE); + results.push(result); + } + } + + // Upgrade Treasury + if (shouldUpgrade.Treasury) { + if (!DEFAULT_ADDRESSES.TREASURY) { + console.log('\nSkipping Treasury: TREASURY_ADDRESS not set'); + } else { + const result = await upgradeContract('Treasury', DEFAULT_ADDRESSES.TREASURY); + results.push(result); + } + } + + // Wait for confirmations then verify + if (results.some((r) => r.upgraded)) { + console.log('\nWaiting for confirmations before verification...'); + await new Promise((resolve) => setTimeout(resolve, 30000)); + + console.log('\nVerifying upgraded contracts...'); + for (const result of results) { + if (result.upgraded) { + await verifyContract(result.newImplementation, result.name); + } + } + } + + // Summary + console.log('\n========================================'); + console.log('UPGRADE SUMMARY'); + console.log('========================================'); + + if (results.length === 0) { + console.log('No contracts were upgraded. Check your environment variables.'); + } else { + for (const result of results) { + console.log(`\n${result.name}:`); + console.log(` Proxy: ${result.proxyAddress}`); + console.log(` Old Implementation: ${result.oldImplementation}`); + console.log(` New Implementation: ${result.newImplementation}`); + console.log(` Upgraded: ${result.upgraded ? 'YES' : 'NO'}`); + } + } + + console.log('\n========================================'); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error('Upgrade failed:', error); + process.exit(1); + }); From bb761c423e51b4e97473ee68a2f4e19c11990935 Mon Sep 17 00:00:00 2001 From: ogarciarevett Date: Thu, 5 Feb 2026 17:57:59 +0100 Subject: [PATCH 3/4] Test: Fixing tests --- contracts/upgradeables/soulbounds/Rewards.sol | 42 ++- .../upgradeables/soulbounds/Treasury.sol | 34 ++- test/rewardsNftTreasury.test.ts | 242 ++++++++++-------- test/rewardsSoulbound.test.ts | 185 ++++++++----- 4 files changed, 293 insertions(+), 210 deletions(-) diff --git a/contracts/upgradeables/soulbounds/Rewards.sol b/contracts/upgradeables/soulbounds/Rewards.sol index b020b37..f2780c7 100644 --- a/contracts/upgradeables/soulbounds/Rewards.sol +++ b/contracts/upgradeables/soulbounds/Rewards.sol @@ -297,7 +297,9 @@ contract Rewards is // Validate token inputs _validateTokenInputs(_token); - // Validate ERC20 tokens are whitelisted and reserve amounts + // Validate tokens are whitelisted and reserve amounts + // Assets are held in Treasury contract + address treasuryAddr = treasury; for (uint256 i = 0; i < _token.rewards.length; i++) { LibItems.Reward memory reward = _token.rewards[i]; if (reward.rewardType == LibItems.RewardType.ERC20) { @@ -305,9 +307,7 @@ contract Rewards is revert TokenNotWhitelisted(); } uint256 totalAmount = reward.rewardAmount * _token.maxSupply; - uint256 balance = IERC20(reward.rewardTokenAddress).balanceOf( - address(this) - ); + uint256 balance = IERC20(reward.rewardTokenAddress).balanceOf(treasuryAddr); uint256 reserved = _state().reservedAmounts(reward.rewardTokenAddress); if (balance < reserved + totalAmount) { revert InsufficientTreasuryBalance(); @@ -320,11 +320,11 @@ contract Rewards is revert TokenNotWhitelisted(); } IERC721 nftContract = IERC721(reward.rewardTokenAddress); - // Verify all tokenIds are owned by this contract and unreserved + // Verify all tokenIds are owned by Treasury and unreserved for (uint256 j = 0; j < reward.rewardTokenIds.length; j++) { uint256 tokenId = reward.rewardTokenIds[j]; - // Check contract owns this NFT and it is not already reserved - if (nftContract.ownerOf(tokenId) != address(this) || _state().isErc721Reserved(reward.rewardTokenAddress, tokenId)) { + // Check Treasury owns this NFT and it is not already reserved + if (nftContract.ownerOf(tokenId) != treasuryAddr || _state().isErc721Reserved(reward.rewardTokenAddress, tokenId)) { revert InsufficientTreasuryBalance(); } } @@ -339,7 +339,7 @@ contract Rewards is revert TokenNotWhitelisted(); } uint256 totalAmount = reward.rewardAmount * _token.maxSupply; - uint256 balance = IERC1155(reward.rewardTokenAddress).balanceOf(address(this), reward.rewardTokenId); + uint256 balance = IERC1155(reward.rewardTokenAddress).balanceOf(treasuryAddr, reward.rewardTokenId); uint256 reserved = _state().erc1155ReservedAmounts(reward.rewardTokenAddress, reward.rewardTokenId); if (balance < reserved + totalAmount) { revert InsufficientTreasuryBalance(); @@ -758,19 +758,16 @@ contract Rewards is function _distributeReward(address _to, uint256 _rewardTokenId) private { LibItems.RewardToken memory _rewardToken = _state().getRewardToken(_rewardTokenId); LibItems.Reward[] memory rewards = _rewardToken.rewards; + Treasury treasuryContract = Treasury(treasury); for (uint256 i = 0; i < rewards.length; i++) { LibItems.Reward memory reward = rewards[i]; - address _from = address(this); if (reward.rewardType == LibItems.RewardType.ETHER) { _transferEther(payable(_to), reward.rewardAmount); } else if (reward.rewardType == LibItems.RewardType.ERC20) { - _transferERC20( - IERC20(reward.rewardTokenAddress), - _to, - reward.rewardAmount - ); + // Distribute from Treasury + treasuryContract.distributeERC20(reward.rewardTokenAddress, _to, reward.rewardAmount); // Reduce reserved amount _state().decreaseERC20Reserved(reward.rewardTokenAddress, reward.rewardAmount); } else if (reward.rewardType == LibItems.RewardType.ERC721) { @@ -785,12 +782,8 @@ contract Rewards is // Release reservation _state().releaseERC721(reward.rewardTokenAddress, tokenId); - _transferERC721( - IERC721(reward.rewardTokenAddress), - _from, - _to, - tokenId - ); + // Distribute from Treasury + treasuryContract.distributeERC721(reward.rewardTokenAddress, _to, tokenId); } _state().incrementERC721RewardIndex(_rewardTokenId, i); @@ -798,13 +791,8 @@ contract Rewards is // Release reservation _state().decreaseERC1155Reserved(reward.rewardTokenAddress, reward.rewardTokenId, reward.rewardAmount); - _transferERC1155( - IERC1155(reward.rewardTokenAddress), - _from, - _to, - reward.rewardTokenId, - reward.rewardAmount - ); + // Distribute from Treasury + treasuryContract.distributeERC1155(reward.rewardTokenAddress, _to, reward.rewardTokenId, reward.rewardAmount); } } diff --git a/contracts/upgradeables/soulbounds/Treasury.sol b/contracts/upgradeables/soulbounds/Treasury.sol index 4fb1ae5..3229335 100644 --- a/contracts/upgradeables/soulbounds/Treasury.sol +++ b/contracts/upgradeables/soulbounds/Treasury.sol @@ -144,6 +144,22 @@ contract Treasury is Initializable, AccessControlUpgradeable, UUPSUpgradeable, E IERC1155(_token).safeTransferFrom(address(this), _to, _tokenId, _amount, ""); } + /*////////////////////////////////////////////////////////////// + DISTRIBUTION FUNCTIONS (for claims) + //////////////////////////////////////////////////////////////*/ + + function distributeERC20(address _token, address _to, uint256 _amount) external onlyRole(REWARDS_MANAGER_ROLE) { + SafeERC20.safeTransfer(IERC20(_token), _to, _amount); + } + + function distributeERC721(address _token, address _to, uint256 _tokenId) external onlyRole(REWARDS_MANAGER_ROLE) { + IERC721(_token).safeTransferFrom(address(this), _to, _tokenId); + } + + function distributeERC1155(address _token, address _to, uint256 _tokenId, uint256 _amount) external onlyRole(REWARDS_MANAGER_ROLE) { + IERC1155(_token).safeTransferFrom(address(this), _to, _tokenId, _amount, ""); + } + /*////////////////////////////////////////////////////////////// TREASURY VIEW FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -204,16 +220,16 @@ contract Treasury is Initializable, AccessControlUpgradeable, UUPSUpgradeable, E return (addresses, totalBalances, reservedBalances, availableBalances, symbols, names, types); } - function getTreasuryBalance(address rewardsContract, address _token) external view returns (uint256) { - return IERC20(_token).balanceOf(rewardsContract); + function getTreasuryBalance(address, address _token) external view returns (uint256) { + return IERC20(_token).balanceOf(address(this)); } function getReservedAmount(address, address _token) external view returns (uint256) { return rewardsState.reservedAmounts(_token); } - function getAvailableTreasuryBalance(address rewardsContract, address _token) external view returns (uint256) { - uint256 balance = IERC20(_token).balanceOf(rewardsContract); + function getAvailableTreasuryBalance(address, address _token) external view returns (uint256) { + uint256 balance = IERC20(_token).balanceOf(address(this)); uint256 reserved = rewardsState.reservedAmounts(_token); return balance > reserved ? balance - reserved : 0; } @@ -231,7 +247,7 @@ contract Treasury is Initializable, AccessControlUpgradeable, UUPSUpgradeable, E //////////////////////////////////////////////////////////////*/ function _processERC20Token( - address rewardsContract, + address, address tokenAddress, uint256 index, uint256[] memory totalBalances, @@ -241,7 +257,7 @@ contract Treasury is Initializable, AccessControlUpgradeable, UUPSUpgradeable, E string[] memory names, string[] memory types ) private view { - uint256 totalBalance = IERC20(tokenAddress).balanceOf(rewardsContract); + uint256 totalBalance = IERC20(tokenAddress).balanceOf(address(this)); uint256 reserved = rewardsState.reservedAmounts(tokenAddress); totalBalances[index] = totalBalance; @@ -264,7 +280,7 @@ contract Treasury is Initializable, AccessControlUpgradeable, UUPSUpgradeable, E } function _processERC721Token( - address rewardsContract, + address, address tokenAddress, uint256 index, uint256[] memory totalBalances, @@ -274,7 +290,7 @@ contract Treasury is Initializable, AccessControlUpgradeable, UUPSUpgradeable, E string[] memory names, string[] memory types ) private view { - uint256 totalBalance = IERC721(tokenAddress).balanceOf(rewardsContract); + uint256 totalBalance = IERC721(tokenAddress).balanceOf(address(this)); uint256 reserved = rewardsState.erc721TotalReserved(tokenAddress); totalBalances[index] = totalBalance; @@ -343,7 +359,7 @@ contract Treasury is Initializable, AccessControlUpgradeable, UUPSUpgradeable, E addresses[currentIndex] = erc1155Address; - uint256 balance = IERC1155(erc1155Address).balanceOf(rewardsContract, erc1155TokenId); + uint256 balance = IERC1155(erc1155Address).balanceOf(address(this), erc1155TokenId); uint256 reserved = rewardsState.erc1155ReservedAmounts(erc1155Address, erc1155TokenId); totalBalances[currentIndex] = balance; diff --git a/test/rewardsNftTreasury.test.ts b/test/rewardsNftTreasury.test.ts index ad9bf2f..b70f506 100644 --- a/test/rewardsNftTreasury.test.ts +++ b/test/rewardsNftTreasury.test.ts @@ -35,6 +35,15 @@ describe('Rewards NFT Treasury', function () { const accessToken = await AccessToken.deploy(devWallet.address); await accessToken.waitForDeployment(); + // Deploy RewardsState (UUPS proxy) + const RewardsState = await ethers.getContractFactory('RewardsState'); + const rewardsState = await upgrades.deployProxy( + RewardsState, + [devWallet.address], + { kind: 'uups', initializer: 'initialize' } + ); + await rewardsState.waitForDeployment(); + // Deploy Rewards contract (UUPS proxy) const Rewards = await ethers.getContractFactory('Rewards'); const rewards = await upgrades.deployProxy( @@ -44,6 +53,24 @@ describe('Rewards NFT Treasury', function () { ); await rewards.waitForDeployment(); + // Deploy Treasury (UUPS proxy) + const Treasury = await ethers.getContractFactory('Treasury'); + const treasury = await upgrades.deployProxy( + Treasury, + [devWallet.address, rewards.target, rewardsState.target], + { kind: 'uups', initializer: 'initialize' } + ); + await treasury.waitForDeployment(); + + // Configure Rewards with Treasury and RewardsState + await rewards.setTreasury(treasury.target); + await rewards.setRewardsState(rewardsState.target); + + // Grant STATE_MANAGER_ROLE to Rewards and Treasury on RewardsState + const STATE_MANAGER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('STATE_MANAGER_ROLE')); + await rewardsState.grantRole(STATE_MANAGER_ROLE, rewards.target); + await rewardsState.grantRole(STATE_MANAGER_ROLE, treasury.target); + // Initialize AccessToken with Rewards as minter await accessToken.initialize( 'G7Reward', @@ -54,18 +81,24 @@ describe('Rewards NFT Treasury', function () { rewards.target ); + // Grant MINTER_ROLE to Rewards on AccessToken + const MINTER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('MINTER_ROLE')); + await accessToken.grantRole(MINTER_ROLE, rewards.target); + // Whitelist tokens (unified whitelist for all token types) await rewards.connect(managerWallet).whitelistToken(mockERC20.target, 1); // ERC20 await rewards.connect(managerWallet).whitelistToken(mockERC721.target, 2); // ERC721 await rewards.connect(managerWallet).whitelistToken(mockERC1155.target, 3); // ERC1155 - // Deposit ERC20 to treasury + // Deposit ERC20 to treasury (approve Treasury, not Rewards) await mockERC20.mint(managerWallet.address, ethers.parseEther('10000')); - await mockERC20.connect(managerWallet).approve(rewards.target, ethers.parseEther('10000')); + await mockERC20.connect(managerWallet).approve(treasury.target, ethers.parseEther('10000')); await rewards.connect(managerWallet).depositToTreasury(mockERC20.target, ethers.parseEther('10000')); return { rewards, + treasury, + rewardsState, accessToken, mockERC20, mockERC721, @@ -82,12 +115,12 @@ describe('Rewards NFT Treasury', function () { describe('ERC721 Treasury Management', function () { describe('Create Reward with ERC721', function () { it('Should create reward using ERC721 transferred directly to contract', async function () { - const { rewards, mockERC721, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + const { rewards, treasury, rewardsState, mockERC721, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); - // Admin transfers NFTs directly to contract + // Admin transfers NFTs directly to Treasury (where assets are held) for (let i = 0; i < 10; i++) { await mockERC721.mint(managerWallet.address); - await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, i); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, treasury.target, i); } // Create reward with ERC721 @@ -110,11 +143,11 @@ describe('Rewards NFT Treasury', function () { .to.emit(rewards, 'TokenAdded') .withArgs(1); - // Verify NFTs are reserved + // Verify NFTs are reserved (check RewardsState) for (let i = 0; i < 10; i++) { - expect(await rewards.isErc721Reserved(mockERC721.target, i)).to.be.true; + expect(await rewardsState.isErc721Reserved(mockERC721.target, i)).to.be.true; } - expect(await rewards.erc721TotalReserved(mockERC721.target)).to.equal(10); + expect(await rewardsState.erc721TotalReserved(mockERC721.target)).to.equal(10); }); it('Should revert if ERC721 not owned by contract', async function () { @@ -145,16 +178,16 @@ describe('Rewards NFT Treasury', function () { }); it('Should revert if ERC721 not whitelisted', async function () { - const { rewards, managerWallet, devWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + const { rewards, treasury, managerWallet, devWallet } = await loadFixture(deployRewardsNftTreasuryFixture); // Deploy a new ERC721 that's not whitelisted const MockERC721 = await ethers.getContractFactory('MockERC721'); const notWhitelisted = await MockERC721.deploy(); await notWhitelisted.waitForDeployment(); - // Mint and transfer to contract + // Mint and transfer to Treasury await notWhitelisted.mint(managerWallet.address); - await notWhitelisted.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, 0); + await notWhitelisted.connect(managerWallet).transferFrom(managerWallet.address, treasury.target, 0); const rewardToken = { tokenId: 1, @@ -176,12 +209,12 @@ describe('Rewards NFT Treasury', function () { }); it('Should revert if ERC721 already reserved', async function () { - const { rewards, mockERC721, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + const { rewards, treasury, mockERC721, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); - // Transfer 4 NFTs to contract + // Transfer 4 NFTs to Treasury for (let i = 0; i < 4; i++) { await mockERC721.mint(managerWallet.address); - await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, i); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, treasury.target, i); } // Create first reward using tokenIds 0 and 1 @@ -224,38 +257,33 @@ describe('Rewards NFT Treasury', function () { }); describe('Withdraw ERC721', function () { - it('Should withdraw unreserved ERC721 via withdrawAssets', async function () { - const { rewards, mockERC721, managerWallet, user1 } = await loadFixture( + it('Should withdraw unreserved ERC721 via withdrawERC721UnreservedTreasury', async function () { + const { rewards, treasury, mockERC721, managerWallet, user1 } = await loadFixture( deployRewardsNftTreasuryFixture ); - // Transfer NFT to contract (not part of any reward) + // Transfer NFT to Treasury (not part of any reward) await mockERC721.mint(managerWallet.address); - await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, 0); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, treasury.target, 0); - await expect( - rewards.connect(managerWallet).withdrawAssets( - 2, // LibItems.RewardType.ERC721 - user1.address, - mockERC721.target, - [0], - [] - ) - ) - .to.emit(rewards, 'AssetsWithdrawn') - .withArgs(2, user1.address, 0); + // Withdraw unreserved NFT from Treasury + await rewards.connect(managerWallet).withdrawERC721UnreservedTreasury( + mockERC721.target, + user1.address, + 0 + ); expect(await mockERC721.ownerOf(0)).to.equal(user1.address); }); it('Should revert withdraw for reserved ERC721', async function () { - const { rewards, mockERC721, managerWallet, user1 } = await loadFixture( + const { rewards, treasury, mockERC721, managerWallet, user1 } = await loadFixture( deployRewardsNftTreasuryFixture ); - // Transfer NFT to contract + // Transfer NFT to Treasury await mockERC721.mint(managerWallet.address); - await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, 0); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, treasury.target, 0); // Create reward to reserve the NFT const rewardToken = { @@ -274,16 +302,14 @@ describe('Rewards NFT Treasury', function () { }; await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); - // Try to withdraw reserved NFT via withdrawAssets + // Try to withdraw reserved NFT from Treasury await expect( - rewards.connect(managerWallet).withdrawAssets( - 2, // ERC721 - user1.address, + rewards.connect(managerWallet).withdrawERC721UnreservedTreasury( mockERC721.target, - [0], - [] + user1.address, + 0 ) - ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + ).to.be.revertedWithCustomError(treasury, 'InsufficientTreasuryBalance'); }); }); }); @@ -291,11 +317,11 @@ describe('Rewards NFT Treasury', function () { describe('ERC1155 Treasury Management', function () { describe('Create Reward with ERC1155', function () { it('Should create reward using ERC1155 transferred directly to contract', async function () { - const { rewards, mockERC1155, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + const { rewards, treasury, rewardsState, mockERC1155, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); - // Admin transfers ERC1155 directly to contract + // Admin transfers ERC1155 directly to Treasury await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); - await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, rewards.target, 1, 100, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, treasury.target, 1, 100, '0x'); // Create reward with ERC1155 const rewardToken = { @@ -317,16 +343,16 @@ describe('Rewards NFT Treasury', function () { .to.emit(rewards, 'TokenAdded') .withArgs(1); - // Verify amount is reserved - expect(await rewards.erc1155ReservedAmounts(mockERC1155.target, 1)).to.equal(100); + // Verify amount is reserved (check RewardsState) + expect(await rewardsState.erc1155ReservedAmounts(mockERC1155.target, 1)).to.equal(100); }); it('Should revert if insufficient ERC1155 balance', async function () { - const { rewards, mockERC1155, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + const { rewards, treasury, mockERC1155, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); - // Transfer only 50 tokens to contract + // Transfer only 50 tokens to Treasury await mockERC1155.mint(managerWallet.address, 1, 50, '0x'); - await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, rewards.target, 1, 50, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, treasury.target, 1, 50, '0x'); // Try to create reward requiring 100 tokens const rewardToken = { @@ -351,39 +377,35 @@ describe('Rewards NFT Treasury', function () { }); describe('Withdraw ERC1155', function () { - it('Should withdraw unreserved ERC1155 via withdrawAssets', async function () { - const { rewards, mockERC1155, managerWallet, user1 } = await loadFixture( + it('Should withdraw unreserved ERC1155 via withdrawERC1155UnreservedTreasury', async function () { + const { rewards, treasury, mockERC1155, managerWallet, user1 } = await loadFixture( deployRewardsNftTreasuryFixture ); - // Transfer tokens to contract (not part of any reward) + // Transfer tokens to Treasury (not part of any reward) await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); await mockERC1155 .connect(managerWallet) - .safeTransferFrom(managerWallet.address, rewards.target, 1, 100, '0x'); + .safeTransferFrom(managerWallet.address, treasury.target, 1, 100, '0x'); - await expect( - rewards.connect(managerWallet).withdrawAssets( - 3, // LibItems.RewardType.ERC1155 - user1.address, - mockERC1155.target, - [1], - [50] - ) - ) - .to.emit(rewards, 'AssetsWithdrawn') - .withArgs(3, user1.address, 50); + // Withdraw unreserved ERC1155 from Treasury + await rewards.connect(managerWallet).withdrawERC1155UnreservedTreasury( + mockERC1155.target, + user1.address, + 1, + 50 + ); expect(await mockERC1155.balanceOf(user1.address, 1)).to.equal(50); - expect(await mockERC1155.balanceOf(rewards.target, 1)).to.equal(50); + expect(await mockERC1155.balanceOf(treasury.target, 1)).to.equal(50); }); it('Should revert if withdraw amount exceeds unreserved', async function () { - const { rewards, mockERC1155, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + const { rewards, treasury, mockERC1155, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); - // Transfer tokens to contract + // Transfer tokens to Treasury await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); - await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, rewards.target, 1, 100, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, treasury.target, 1, 100, '0x'); // Create reward reserving 80 tokens const rewardToken = { @@ -404,26 +426,25 @@ describe('Rewards NFT Treasury', function () { // Try to withdraw 30 (only 20 unreserved) await expect( - rewards.connect(managerWallet).withdrawAssets( - 3, // ERC1155 - user1.address, + rewards.connect(managerWallet).withdrawERC1155UnreservedTreasury( mockERC1155.target, - [1], - [30] + user1.address, + 1, + 30 ) - ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + ).to.be.revertedWithCustomError(treasury, 'InsufficientBalance'); }); }); }); describe('Claim Rewards', function () { it('Should distribute ERC721 on claim and release reservation', async function () { - const { rewards, accessToken, mockERC721, managerWallet, minterWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + const { rewards, treasury, rewardsState, accessToken, mockERC721, managerWallet, minterWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); - // Transfer NFTs to contract + // Transfer NFTs to Treasury for (let i = 0; i < 2; i++) { await mockERC721.mint(managerWallet.address); - await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, i); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, treasury.target, i); } // Create reward @@ -457,24 +478,24 @@ describe('Rewards NFT Treasury', function () { // Verify user received NFT expect(await mockERC721.ownerOf(0)).to.equal(user1.address); - // Verify reservation released - expect(await rewards.isErc721Reserved(mockERC721.target, 0)).to.be.false; + // Verify reservation released (check RewardsState) + expect(await rewardsState.isErc721Reserved(mockERC721.target, 0)).to.be.false; // TokenId 1 should still be reserved - expect(await rewards.isErc721Reserved(mockERC721.target, 1)).to.be.true; - expect(await rewards.erc721TotalReserved(mockERC721.target)).to.equal(1); + expect(await rewardsState.isErc721Reserved(mockERC721.target, 1)).to.be.true; + expect(await rewardsState.erc721TotalReserved(mockERC721.target)).to.equal(1); }); it('Should distribute ERC1155 on claim', async function () { - const { rewards, accessToken, mockERC1155, managerWallet, minterWallet, user1 } = await loadFixture( + const { rewards, treasury, rewardsState, accessToken, mockERC1155, managerWallet, minterWallet, user1 } = await loadFixture( deployRewardsNftTreasuryFixture ); - // Transfer tokens to contract + // Transfer tokens to Treasury await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); await mockERC1155 .connect(managerWallet) - .safeTransferFrom(managerWallet.address, rewards.target, 1, 100, '0x'); + .safeTransferFrom(managerWallet.address, treasury.target, 1, 100, '0x'); // Create reward const rewardToken = { @@ -504,22 +525,22 @@ describe('Rewards NFT Treasury', function () { // Verify user received tokens expect(await mockERC1155.balanceOf(user1.address, 1)).to.equal(10); - // Verify reserved amount decreased after claim - expect(await rewards.erc1155ReservedAmounts(mockERC1155.target, 1)).to.equal(90); + // Verify reserved amount decreased after claim (check RewardsState) + expect(await rewardsState.erc1155ReservedAmounts(mockERC1155.target, 1)).to.equal(90); }); }); describe('Mixed Rewards (ERC20 + ERC721 + ERC1155)', function () { it('Should create and claim reward with mixed asset types', async function () { - const { rewards, accessToken, mockERC20, mockERC721, mockERC1155, managerWallet, minterWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + const { rewards, treasury, accessToken, mockERC20, mockERC721, mockERC1155, managerWallet, minterWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); - // Transfer ERC721 to contract + // Transfer ERC721 to Treasury await mockERC721.mint(managerWallet.address); - await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, 0); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, treasury.target, 0); - // Transfer ERC1155 to contract + // Transfer ERC1155 to Treasury await mockERC1155.mint(managerWallet.address, 1, 10, '0x'); - await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, rewards.target, 1, 10, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, treasury.target, 1, 10, '0x'); // Create mixed reward const rewardToken = { @@ -569,13 +590,13 @@ describe('Rewards NFT Treasury', function () { }); - describe('withdrawAssets Protection', function () { - it('Should protect reserved ERC721 via withdrawAssets', async function () { - const { rewards, mockERC721, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + describe('Treasury Withdrawal Protection', function () { + it('Should protect reserved ERC721 via withdrawERC721UnreservedTreasury', async function () { + const { rewards, treasury, mockERC721, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); - // Transfer NFT to contract + // Transfer NFT to Treasury await mockERC721.mint(managerWallet.address); - await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, 0); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, treasury.target, 0); // Create reward reserving the NFT const rewardToken = { @@ -594,24 +615,22 @@ describe('Rewards NFT Treasury', function () { }; await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); - // Try to withdraw reserved NFT via withdrawAssets + // Try to withdraw reserved NFT from Treasury await expect( - rewards.connect(managerWallet).withdrawAssets( - 2, // ERC721 - user1.address, + rewards.connect(managerWallet).withdrawERC721UnreservedTreasury( mockERC721.target, - [0], - [] + user1.address, + 0 ) - ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + ).to.be.revertedWithCustomError(treasury, 'InsufficientTreasuryBalance'); }); - it('Should protect reserved ERC1155 via withdrawAssets', async function () { - const { rewards, mockERC1155, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + it('Should protect reserved ERC1155 via withdrawERC1155UnreservedTreasury', async function () { + const { rewards, treasury, mockERC1155, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); - // Transfer tokens to contract + // Transfer tokens to Treasury await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); - await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, rewards.target, 1, 100, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, treasury.target, 1, 100, '0x'); // Create reward reserving 80 tokens const rewardToken = { @@ -630,16 +649,15 @@ describe('Rewards NFT Treasury', function () { }; await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); - // Try to withdraw 30 via withdrawAssets (only 20 unreserved) + // Try to withdraw 30 from Treasury (only 20 unreserved) await expect( - rewards.connect(managerWallet).withdrawAssets( - 3, // ERC1155 - user1.address, + rewards.connect(managerWallet).withdrawERC1155UnreservedTreasury( mockERC1155.target, - [1], - [30] + user1.address, + 1, + 30 ) - ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + ).to.be.revertedWithCustomError(treasury, 'InsufficientBalance'); }); }); }); diff --git a/test/rewardsSoulbound.test.ts b/test/rewardsSoulbound.test.ts index 9675a13..9a2f9e2 100644 --- a/test/rewardsSoulbound.test.ts +++ b/test/rewardsSoulbound.test.ts @@ -39,6 +39,15 @@ describe('Rewards with Soulbound Tokens', function () { ); await soulboundBadge.waitForDeployment(); + // Deploy RewardsState (UUPS proxy) + const RewardsState = await ethers.getContractFactory('RewardsState'); + const rewardsState = await upgrades.deployProxy( + RewardsState, + [devWallet.address], + { kind: 'uups', initializer: 'initialize' } + ); + await rewardsState.waitForDeployment(); + // Deploy Rewards contract (UUPS proxy) const Rewards = await ethers.getContractFactory('Rewards'); const rewards = await upgrades.deployProxy( @@ -48,6 +57,24 @@ describe('Rewards with Soulbound Tokens', function () { ); await rewards.waitForDeployment(); + // Deploy Treasury (UUPS proxy) + const Treasury = await ethers.getContractFactory('Treasury'); + const treasury = await upgrades.deployProxy( + Treasury, + [devWallet.address, rewards.target, rewardsState.target], + { kind: 'uups', initializer: 'initialize' } + ); + await treasury.waitForDeployment(); + + // Configure Rewards with Treasury and RewardsState + await rewards.setTreasury(treasury.target); + await rewards.setRewardsState(rewardsState.target); + + // Grant STATE_MANAGER_ROLE to Rewards and Treasury on RewardsState + const STATE_MANAGER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('STATE_MANAGER_ROLE')); + await rewardsState.grantRole(STATE_MANAGER_ROLE, rewards.target); + await rewardsState.grantRole(STATE_MANAGER_ROLE, treasury.target); + // Initialize AccessToken with Rewards as minter await accessToken.initialize( 'G7Reward', @@ -58,6 +85,10 @@ describe('Rewards with Soulbound Tokens', function () { rewards.target ); + // Grant MINTER_ROLE to Rewards on AccessToken + const MINTER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('MINTER_ROLE')); + await accessToken.grantRole(MINTER_ROLE, rewards.target); + // Setup: Add a token to the soulbound badge contract const badgeTokenId = 1; await soulboundBadge.connect(devWallet).addNewToken({ @@ -70,13 +101,15 @@ describe('Rewards with Soulbound Tokens', function () { // Whitelist ERC20 for treasury await rewards.connect(managerWallet).whitelistToken(mockERC20.target, 1); // ERC20 - // Mint ERC20 to manager and deposit to treasury + // Mint ERC20 to manager and deposit to treasury (approve Treasury, not Rewards) await mockERC20.mint(managerWallet.address, ethers.parseEther('1000')); - await mockERC20.connect(managerWallet).approve(rewards.target, ethers.parseEther('1000')); + await mockERC20.connect(managerWallet).approve(treasury.target, ethers.parseEther('1000')); await rewards.connect(managerWallet).depositToTreasury(mockERC20.target, ethers.parseEther('1000')); return { rewards, + treasury, + rewardsState, accessToken, soulboundBadge, mockERC20, @@ -92,56 +125,55 @@ describe('Rewards with Soulbound Tokens', function () { describe('ERC1155 Soulbound Badge as Reward', function () { it('Should FAIL to transfer soulbound badge WITHOUT whitelist', async function () { - const { soulboundBadge, rewards, devWallet, user1, badgeTokenId } = + const { soulboundBadge, rewards, treasury, devWallet, user1, badgeTokenId } = await loadFixture(deployRewardsWithSoulboundFixture); - // Mint soulbound badge to Rewards contract (as if deposited) + // Mint soulbound badge to Treasury contract (as if deposited) // The devWallet has MINTER_ROLE, so it can mint await soulboundBadge.connect(devWallet).adminMintId( - rewards.target, // mint to Rewards contract + treasury.target, // mint to Treasury contract badgeTokenId, 10, // amount true // soulbound = true ); - // Verify Rewards contract has the badges - expect(await soulboundBadge.balanceOf(rewards.target, badgeTokenId)).to.equal(10); + // Verify Treasury contract has the badges + expect(await soulboundBadge.balanceOf(treasury.target, badgeTokenId)).to.equal(10); - // Without whitelist, Rewards contract cannot transfer soulbound badges + // Without whitelist, Treasury contract cannot transfer soulbound badges // This simulates what would happen if we tried to use soulbound badges as ERC1155 rewards // The safeTransferFrom will fail because: - // 1. Rewards contract is not whitelisted + // 1. Treasury contract is not whitelisted // 2. The badges are soulbound await expect( soulboundBadge .connect(devWallet) // Even admin cannot transfer soulbound tokens - .safeTransferFrom(rewards.target, user1.address, badgeTokenId, 1, '0x') + .safeTransferFrom(treasury.target, user1.address, badgeTokenId, 1, '0x') ).to.be.revertedWithCustomError(soulboundBadge, 'SoulboundAmountError'); }); it('Should SUCCEED to transfer soulbound badge WITH whitelist', async function () { - const { soulboundBadge, rewards, devWallet, user1, badgeTokenId } = + const { soulboundBadge, treasury, devWallet, user1, badgeTokenId } = await loadFixture(deployRewardsWithSoulboundFixture); - // IMPORTANT: Whitelist the Rewards contract on the soulbound badge contract - await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + // IMPORTANT: Whitelist the Treasury contract on the soulbound badge contract + await soulboundBadge.connect(devWallet).updateWhitelistAddress(treasury.target, true); // Mint soulbound badge to devWallet first await soulboundBadge.connect(devWallet).adminMintId(devWallet.address, badgeTokenId, 10, true); - // DevWallet transfers to Rewards contract (works because devWallet is whitelisted as sender) + // DevWallet transfers to Treasury contract (works because devWallet is whitelisted as sender) await soulboundBadge.connect(devWallet).updateWhitelistAddress(devWallet.address, true); - await soulboundBadge.connect(devWallet).safeTransferFrom(devWallet.address, rewards.target, badgeTokenId, 10, '0x'); + await soulboundBadge.connect(devWallet).safeTransferFrom(devWallet.address, treasury.target, badgeTokenId, 10, '0x'); - // Verify Rewards contract has the badges - expect(await soulboundBadge.balanceOf(rewards.target, badgeTokenId)).to.equal(10); + // Verify Treasury contract has the badges + expect(await soulboundBadge.balanceOf(treasury.target, badgeTokenId)).to.equal(10); - // Now simulate what the Rewards contract would do internally when distributing rewards - // The Rewards contract (whitelisted) can transfer soulbound badges to users - // In real usage, this happens in _distributeReward via _transferERC1155 + // Now simulate what the Treasury contract would do internally when distributing rewards + // The Treasury contract (whitelisted) can transfer soulbound badges to users + // In real usage, this happens in _distributeReward via Treasury.distributeERC1155 - // For this test, we use withdrawAssets to demonstrate the mechanism - // Note: withdrawAssets requires MANAGER_ROLE and would use the contract's internal transfer + // For this test, the Treasury holds assets and Rewards calls Treasury to distribute }); it('User CANNOT transfer soulbound badge after receiving (still soulbound to user)', async function () { @@ -177,6 +209,8 @@ describe('Rewards with Soulbound Tokens', function () { it('Complete flow: Create reward with ERC1155 soulbound badge as prize', async function () { const { rewards, + treasury, + rewardsState, accessToken, soulboundBadge, mockERC20, @@ -187,24 +221,24 @@ describe('Rewards with Soulbound Tokens', function () { badgeTokenId, } = await loadFixture(deployRewardsWithSoulboundFixture); - // Step 1: Whitelist Rewards contract AND managerWallet on soulbound badge contract - // - Rewards contract needs whitelist to RECEIVE and DISTRIBUTE soulbound badges - // - ManagerWallet needs whitelist to TRANSFER soulbound badges to Rewards - await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + // Step 1: Whitelist Treasury contract AND managerWallet on soulbound badge contract + // - Treasury contract needs whitelist to RECEIVE and DISTRIBUTE soulbound badges + // - ManagerWallet needs whitelist to TRANSFER soulbound badges to Treasury + await soulboundBadge.connect(devWallet).updateWhitelistAddress(treasury.target, true); await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); // Step 2: Whitelist the ERC1155 (soulbound badge) in the Rewards treasury (unified whitelistToken) await rewards.connect(managerWallet).whitelistToken(soulboundBadge.target, 3); // ERC1155 - // Step 3: Mint soulbound badges to managerWallet, then transfer to Rewards contract + // Step 3: Mint soulbound badges to managerWallet, then transfer to Treasury contract await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); - await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + await soulboundBadge.connect(managerWallet).setApprovalForAll(treasury.target, true); await soulboundBadge .connect(managerWallet) - .safeTransferFrom(managerWallet.address, rewards.target, badgeTokenId, 10, '0x'); + .safeTransferFrom(managerWallet.address, treasury.target, badgeTokenId, 10, '0x'); - // Verify Rewards contract now has the badges - expect(await soulboundBadge.balanceOf(rewards.target, badgeTokenId)).to.equal(10); + // Verify Treasury contract now has the badges + expect(await soulboundBadge.balanceOf(treasury.target, badgeTokenId)).to.equal(10); // Step 4: Create a reward token that gives: // - 10 ERC20 tokens (from treasury) @@ -235,8 +269,8 @@ describe('Rewards with Soulbound Tokens', function () { // This reserves 10 soulbound badges (1 per maxSupply) await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); - // Verify reserved amounts (ERC1155 treasury tracking) - expect(await rewards.erc1155ReservedAmounts(soulboundBadge.target, badgeTokenId)).to.equal(10); + // Verify reserved amounts (ERC1155 treasury tracking - check RewardsState) + expect(await rewardsState.erc1155ReservedAmounts(soulboundBadge.target, badgeTokenId)).to.equal(10); // Step 5: Mint reward token to user (tokenId, amount, isSoulbound) await rewards.connect(minterWallet).adminMintById(user1.address, rewardTokenId, 1, true); @@ -262,9 +296,9 @@ describe('Rewards with Soulbound Tokens', function () { soulboundBadge.connect(user1).safeTransferFrom(user1.address, devWallet.address, badgeTokenId, 1, '0x') ).to.be.revertedWithCustomError(soulboundBadge, 'SoulboundAmountError'); - // Step 8: Verify Rewards contract still has remaining badges and reserved amount decreased - expect(await soulboundBadge.balanceOf(rewards.target, badgeTokenId)).to.equal(9); - expect(await rewards.erc1155ReservedAmounts(soulboundBadge.target, badgeTokenId)).to.equal(9); + // Step 8: Verify Treasury contract still has remaining badges and reserved amount decreased + expect(await soulboundBadge.balanceOf(treasury.target, badgeTokenId)).to.equal(9); + expect(await rewardsState.erc1155ReservedAmounts(soulboundBadge.target, badgeTokenId)).to.equal(9); }); }); @@ -377,6 +411,15 @@ describe('Rewards with Soulbound Tokens', function () { const accessToken = await AccessToken.deploy(devWallet.address); await accessToken.waitForDeployment(); + // Deploy RewardsState (UUPS proxy) + const RewardsState = await ethers.getContractFactory('RewardsState'); + const rewardsState = await upgrades.deployProxy( + RewardsState, + [devWallet.address], + { kind: 'uups', initializer: 'initialize' } + ); + await rewardsState.waitForDeployment(); + // Deploy Rewards using UUPS proxy const Rewards = await ethers.getContractFactory('Rewards'); const rewards = await upgrades.deployProxy( @@ -386,6 +429,24 @@ describe('Rewards with Soulbound Tokens', function () { ); await rewards.waitForDeployment(); + // Deploy Treasury (UUPS proxy) + const Treasury = await ethers.getContractFactory('Treasury'); + const treasury = await upgrades.deployProxy( + Treasury, + [devWallet.address, rewards.target, rewardsState.target], + { kind: 'uups', initializer: 'initialize' } + ); + await treasury.waitForDeployment(); + + // Configure Rewards with Treasury and RewardsState + await rewards.setTreasury(treasury.target); + await rewards.setRewardsState(rewardsState.target); + + // Grant STATE_MANAGER_ROLE to Rewards and Treasury on RewardsState + const STATE_MANAGER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('STATE_MANAGER_ROLE')); + await rewardsState.grantRole(STATE_MANAGER_ROLE, rewards.target); + await rewardsState.grantRole(STATE_MANAGER_ROLE, treasury.target); + await accessToken.initialize( 'G7Reward', 'G7R', 'https://example.com/token/', 'https://example.com/contract/', devWallet.address, rewards.target @@ -419,11 +480,11 @@ describe('Rewards with Soulbound Tokens', function () { }); it('Should return ERC1155 badge with type "nft" after creating reward', async function () { - const { rewards, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = + const { rewards, treasury, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = await loadFixture(deployRewardsWithSoulboundFixture); - // Whitelist Rewards and manager on badge contract - await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + // Whitelist Treasury and manager on badge contract + await soulboundBadge.connect(devWallet).updateWhitelistAddress(treasury.target, true); await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); // Whitelist soulboundBadge in Rewards treasury @@ -432,10 +493,10 @@ describe('Rewards with Soulbound Tokens', function () { // Mint badges to manager await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); - // Transfer badges to Rewards contract + // Transfer badges to Treasury contract await soulboundBadge.connect(managerWallet).safeTransferFrom( managerWallet.address, - rewards.target, + treasury.target, badgeTokenId, 10, // Transfer 10 badges (same as maxSupply * rewardAmount) '0x' @@ -473,11 +534,11 @@ describe('Rewards with Soulbound Tokens', function () { }); it('Should return both ERC20 and ERC1155 with correct types in mixed reward', async function () { - const { rewards, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = + const { rewards, treasury, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = await loadFixture(deployRewardsWithSoulboundFixture); - // Whitelist Rewards and manager on badge contract - await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + // Whitelist Treasury and manager on badge contract + await soulboundBadge.connect(devWallet).updateWhitelistAddress(treasury.target, true); await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); // Whitelist soulboundBadge in Rewards treasury @@ -486,10 +547,10 @@ describe('Rewards with Soulbound Tokens', function () { // Mint badges to manager await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); - // Transfer badges to Rewards contract (5 maxSupply * 2 rewardAmount = 10 badges) + // Transfer badges to Treasury contract (5 maxSupply * 2 rewardAmount = 10 badges) await soulboundBadge.connect(managerWallet).safeTransferFrom( managerWallet.address, - rewards.target, + treasury.target, badgeTokenId, 10, '0x' @@ -538,19 +599,19 @@ describe('Rewards with Soulbound Tokens', function () { }); it('Should update balances after user claims reward', async function () { - const { rewards, accessToken, soulboundBadge, mockERC20, devWallet, managerWallet, minterWallet, user1, badgeTokenId } = + const { rewards, treasury, accessToken, soulboundBadge, mockERC20, devWallet, managerWallet, minterWallet, user1, badgeTokenId } = await loadFixture(deployRewardsWithSoulboundFixture); // Setup: Whitelist and mint badges - await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + await soulboundBadge.connect(devWallet).updateWhitelistAddress(treasury.target, true); await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); await rewards.connect(managerWallet).whitelistToken(soulboundBadge.target, 3); // 3 = ERC1155 await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); - // Transfer badges to Rewards contract (10 maxSupply * 1 rewardAmount = 10 badges) + // Transfer badges to Treasury contract (10 maxSupply * 1 rewardAmount = 10 badges) await soulboundBadge.connect(managerWallet).safeTransferFrom( managerWallet.address, - rewards.target, + treasury.target, badgeTokenId, 10, '0x' @@ -607,7 +668,7 @@ describe('Rewards with Soulbound Tokens', function () { }); it('Should handle multiple ERC1155 badge contracts', async function () { - const { rewards, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = + const { rewards, treasury, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = await loadFixture(deployRewardsWithSoulboundFixture); // Deploy a second ERC1155Soulbound badge contract @@ -627,10 +688,10 @@ describe('Rewards with Soulbound Tokens', function () { feeBasisPoints: 0, }); - // Whitelist Rewards on both badge contracts - await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + // Whitelist Treasury on both badge contracts + await soulboundBadge.connect(devWallet).updateWhitelistAddress(treasury.target, true); await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); - await secondBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + await secondBadge.connect(devWallet).updateWhitelistAddress(treasury.target, true); await secondBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); // Whitelist both badges in Rewards treasury @@ -641,17 +702,17 @@ describe('Rewards with Soulbound Tokens', function () { await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 50, true); await secondBadge.connect(devWallet).adminMintId(managerWallet.address, secondBadgeTokenId, 50, true); - // Transfer badges to Rewards contract + // Transfer badges to Treasury contract await soulboundBadge.connect(managerWallet).safeTransferFrom( managerWallet.address, - rewards.target, + treasury.target, badgeTokenId, 5, // First reward: 5 maxSupply * 1 rewardAmount = 5 badges '0x' ); await secondBadge.connect(managerWallet).safeTransferFrom( managerWallet.address, - rewards.target, + treasury.target, secondBadgeTokenId, 10, // Second reward: 5 maxSupply * 2 rewardAmount = 10 badges '0x' @@ -702,11 +763,11 @@ describe('Rewards with Soulbound Tokens', function () { }); it('Should not duplicate NFT addresses when same badge used in multiple rewards', async function () { - const { rewards, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = + const { rewards, treasury, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = await loadFixture(deployRewardsWithSoulboundFixture); // Whitelist - await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + await soulboundBadge.connect(devWallet).updateWhitelistAddress(treasury.target, true); await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); // Whitelist soulboundBadge in Rewards treasury @@ -725,17 +786,17 @@ describe('Rewards with Soulbound Tokens', function () { await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 50, true); await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, secondTokenId, 50, true); - // Transfer both token IDs to Rewards contract + // Transfer both token IDs to Treasury contract await soulboundBadge.connect(managerWallet).safeTransferFrom( managerWallet.address, - rewards.target, + treasury.target, badgeTokenId, 5, // First reward: 5 maxSupply * 1 rewardAmount = 5 badges '0x' ); await soulboundBadge.connect(managerWallet).safeTransferFrom( managerWallet.address, - rewards.target, + treasury.target, secondTokenId, 5, // Second reward: 5 maxSupply * 1 rewardAmount = 5 badges '0x' From 67a5cc402a6f8b991dfcae15d67e2c24e54a6adb Mon Sep 17 00:00:00 2001 From: ogarciarevett Date: Fri, 6 Feb 2026 03:24:05 +0100 Subject: [PATCH 4/4] Feat: Update rewards module --- contracts/upgradeables/soulbounds/Rewards.sol | 50 +-- .../upgradeables/soulbounds/Treasury.sol | 19 +- scripts/deployAllBadges.ts | 39 +-- scripts/setupAllRewards.ts | 310 +++++++++++++++--- scripts/upgradeRewardsSystem.ts | 45 ++- test/rewardsNftTreasury.test.ts | 173 ++++++++++ 6 files changed, 500 insertions(+), 136 deletions(-) diff --git a/contracts/upgradeables/soulbounds/Rewards.sol b/contracts/upgradeables/soulbounds/Rewards.sol index f2780c7..72e438b 100644 --- a/contracts/upgradeables/soulbounds/Rewards.sol +++ b/contracts/upgradeables/soulbounds/Rewards.sol @@ -557,20 +557,26 @@ contract Rewards is uint256 oldSupply = rewardToken.maxSupply; uint256 newSupply = oldSupply + _additionalSupply; - // Validate treasury has enough balance for ERC20 rewards + // Validate treasury has enough balance for rewards + address treasuryAddr = treasury; for (uint256 i = 0; i < rewardToken.rewards.length; i++) { LibItems.Reward memory reward = rewardToken.rewards[i]; if (reward.rewardType == LibItems.RewardType.ERC20) { uint256 additionalAmount = reward.rewardAmount * _additionalSupply; - uint256 balance = IERC20(reward.rewardTokenAddress).balanceOf( - address(this) - ); + uint256 balance = IERC20(reward.rewardTokenAddress).balanceOf(treasuryAddr); uint256 reserved = _state().reservedAmounts(reward.rewardTokenAddress); if (balance < reserved + additionalAmount) { revert InsufficientTreasuryBalance(); } - // Reserve additional amount _state().increaseERC20Reserved(reward.rewardTokenAddress, additionalAmount); + } else if (reward.rewardType == LibItems.RewardType.ERC1155) { + uint256 additionalAmount = reward.rewardAmount * _additionalSupply; + uint256 balance = IERC1155(reward.rewardTokenAddress).balanceOf(treasuryAddr, reward.rewardTokenId); + uint256 reserved = _state().erc1155ReservedAmounts(reward.rewardTokenAddress, reward.rewardTokenId); + if (balance < reserved + additionalAmount) { + revert InsufficientTreasuryBalance(); + } + _state().increaseERC1155Reserved(reward.rewardTokenAddress, reward.rewardTokenId, additionalAmount); } } @@ -620,48 +626,22 @@ contract Rewards is revert AddressIsZero(); } - address _from = address(this); + Treasury treasuryContract = Treasury(treasury); if (_rewardType == LibItems.RewardType.ETHER) { _transferEther(payable(_to), _amounts[0]); } else if (_rewardType == LibItems.RewardType.ERC20) { - // Check if withdrawal would violate reserved amounts - if (_state().whitelistedTokens(_tokenAddress)) { - uint256 balance = IERC20(_tokenAddress).balanceOf(address(this)); - uint256 reserved = _state().reservedAmounts(_tokenAddress); - if (balance < reserved + _amounts[0]) { - revert InsufficientTreasuryBalance(); - } - } - _transferERC20(IERC20(_tokenAddress), _to, _amounts[0]); + treasuryContract.withdrawUnreservedTreasury(_tokenAddress, _to); } else if (_rewardType == LibItems.RewardType.ERC721) { - IERC721 token = IERC721(_tokenAddress); for (uint256 i = 0; i < _tokenIds.length; i++) { - // Check if NFT is reserved - if (_state().isErc721Reserved(_tokenAddress, _tokenIds[i])) { - revert InsufficientTreasuryBalance(); - } - _transferERC721(token, _from, _to, _tokenIds[i]); + treasuryContract.withdrawERC721UnreservedTreasury(_tokenAddress, _to, _tokenIds[i]); } } else if (_rewardType == LibItems.RewardType.ERC1155) { if (_tokenIds.length != _amounts.length) { revert InvalidLength(); } for (uint256 i = 0; i < _tokenIds.length; i++) { - // Check if amount exceeds unreserved balance - uint256 balance = IERC1155(_tokenAddress).balanceOf(address(this), _tokenIds[i]); - uint256 reserved = _state().erc1155ReservedAmounts(_tokenAddress, _tokenIds[i]); - uint256 available = balance > reserved ? balance - reserved : 0; - if (_amounts[i] > available) { - revert InsufficientTreasuryBalance(); - } - _transferERC1155( - IERC1155(_tokenAddress), - _from, - _to, - _tokenIds[i], - _amounts[i] - ); + treasuryContract.withdrawERC1155UnreservedTreasury(_tokenAddress, _to, _tokenIds[i], _amounts[i]); } } diff --git a/contracts/upgradeables/soulbounds/Treasury.sol b/contracts/upgradeables/soulbounds/Treasury.sol index 3229335..f8ef841 100644 --- a/contracts/upgradeables/soulbounds/Treasury.sol +++ b/contracts/upgradeables/soulbounds/Treasury.sol @@ -23,6 +23,11 @@ interface IRewards { function getTokenRewards(uint256 tokenId) external view returns (LibItems.Reward[] memory); } +interface IERC1155Metadata { + function name() external view returns (string memory); + function symbol() external view returns (string memory); +} + /** * @title Treasury * @notice Treasury contract for managing token deposits, withdrawals, and whitelisting @@ -366,8 +371,18 @@ contract Treasury is Initializable, AccessControlUpgradeable, UUPSUpgradeable, E reservedBalances[currentIndex] = reserved; availableBalances[currentIndex] = balance > reserved ? balance - reserved : 0; - names[currentIndex] = "ERC1155 Collection"; - symbols[currentIndex] = "ERC1155"; + try IERC1155Metadata(erc1155Address).name() returns (string memory _name) { + names[currentIndex] = _name; + } catch { + names[currentIndex] = "ERC1155 Collection"; + } + + try IERC1155Metadata(erc1155Address).symbol() returns (string memory _symbol) { + symbols[currentIndex] = _symbol; + } catch { + symbols[currentIndex] = "ERC1155"; + } + types[currentIndex] = "nft"; currentIndex++; diff --git a/scripts/deployAllBadges.ts b/scripts/deployAllBadges.ts index 8fd2533..91c48dc 100644 --- a/scripts/deployAllBadges.ts +++ b/scripts/deployAllBadges.ts @@ -17,25 +17,22 @@ async function main() { console.log('Account balance:', (await ethers.provider.getBalance(deployer.address)).toString()); // Get Rewards contract address from env or use the deployed one - const REWARDS_CONTRACT = process.env.REWARDS_CONTRACT || '0x5d62C8cfDe4a1B0be2Cc102023F2563bc29221Cc'; + const REWARDS_CONTRACT = process.env.REWARDS_CONTRACT || '0x85974902415e87Ae6F94253648f1033163479e38'; // Badge configurations - // NOTE: KPOP and F1 already deployed, only deploying remaining badges const badges = [ - // Already deployed: 0x3D62Dbe1806437bCA71e52Ee9581dee37a608cd8 - // { - // name: 'KPOP Badges', - // symbol: 'KPOP', - // baseURI: 'https://summon.xyz/kpop/badges/', - // contractURI: 'https://summon.xyz/kpop/contract/', - // }, - // Already deployed: 0xd7B81ABA27cDB68D79aCB1B6E6b198fF4D8EeAd7 - // { - // name: 'F1 Grand Prix VIP Ticket', - // symbol: 'F1VIP', - // baseURI: 'https://summon.xyz/rewards/badges/', - // contractURI: 'https://summon.xyz/rewards/contract/', - // }, + { + name: 'KPOP Badges', + symbol: 'KPOP', + baseURI: 'https://summon.xyz/kpop/badges/', + contractURI: 'https://summon.xyz/kpop/contract/', + }, + { + name: 'F1 Grand Prix VIP Ticket', + symbol: 'F1VIP', + baseURI: 'https://summon.xyz/rewards/badges/', + contractURI: 'https://summon.xyz/rewards/contract/', + }, { name: 'New Jeans New Album', symbol: 'NJALBUM', @@ -127,12 +124,10 @@ async function main() { console.log('\n========================================'); console.log('Environment Variables for Setup Script'); console.log('========================================'); - // Already deployed badges - console.log(`KPOP_BADGES_ADDRESS=0x3D62Dbe1806437bCA71e52Ee9581dee37a608cd8`); - console.log(`F1_BADGES_ADDRESS=0xd7B81ABA27cDB68D79aCB1B6E6b198fF4D8EeAd7`); - // Newly deployed badges - console.log(`NEWJEANS_BADGES_ADDRESS=${deployedBadges[0]?.address || 'NOT_DEPLOYED'}`); - console.log(`QUINCE_BADGES_ADDRESS=${deployedBadges[1]?.address || 'NOT_DEPLOYED'}`); + console.log(`KPOP_BADGES_ADDRESS=${deployedBadges[0].address}`); + console.log(`F1_BADGES_ADDRESS=${deployedBadges[1].address}`); + console.log(`NEWJEANS_BADGES_ADDRESS=${deployedBadges[2].address}`); + console.log(`QUINCE_BADGES_ADDRESS=${deployedBadges[3].address}`); console.log(`MOCK_USDC_ADDRESS=${usdcAddress}`); console.log(`REWARDS_ADDRESS=${REWARDS_CONTRACT}`); diff --git a/scripts/setupAllRewards.ts b/scripts/setupAllRewards.ts index ce1aa71..a07eb17 100644 --- a/scripts/setupAllRewards.ts +++ b/scripts/setupAllRewards.ts @@ -22,12 +22,14 @@ const F1_BADGES_ADDRESS = process.env.F1_BADGES_ADDRESS || '0x1a7a1879bE0C3fD48e const NEWJEANS_BADGES_ADDRESS = process.env.NEWJEANS_BADGES_ADDRESS || '0x4afF7E3F1191b4dEE2a0358417a750C1c6fF9b62'; const QUINCE_BADGES_ADDRESS = process.env.QUINCE_BADGES_ADDRESS || '0x40813d715Ed741C0bA6848763c93aaF75fEA7F55'; const MOCK_USDC_ADDRESS = process.env.MOCK_USDC_ADDRESS || '0x3E3a445731d7881a3729A3898D532D5290733Eb5'; -const REWARDS_ADDRESS = process.env.REWARDS_ADDRESS || '0x4163079Aa7d3ed57755c7278BA4156a826E25Ad4'; +const MOCK_ERC721_ADDRESS = process.env.MOCK_ERC721_ADDRESS || ''; +const REWARDS_ADDRESS = process.env.REWARDS_ADDRESS || '0x08809093Bd3B1d02EC55E263f4350de99557E59C'; // Configuration const BADGES_TO_MINT = 100000; // How many badges to mint per contract const REWARD_MAX_SUPPLY = 10000; // How many rewards can be claimed (10k as requested) -const USDC_REWARD_AMOUNT = ethers.parseUnits('10', 6); // 10 USDC per claim +const USDC_REWARD_AMOUNT = ethers.parseUnits('1000000', 6); // 1,000,000 USDC per claim +const ERC721_NFT_COUNT = 20; // Number of ERC721 NFTs to mint and deposit to Treasury // Reward token IDs const REWARD_TOKEN_IDS = { @@ -36,6 +38,7 @@ const REWARD_TOKEN_IDS = { NEWJEANS: 2002, QUINCE: 2003, USDC: 2004, + ERC721: 3001, }; interface BadgeConfig { @@ -62,28 +65,29 @@ async function setupBadgeReward( } const badge = await ethers.getContractAt('ERC1155Soulbound', badgeConfig.address); + const treasuryAddress = await rewards.treasury(); - // Step 1: Whitelist Rewards contract on badge - console.log('Step 1: Whitelisting Rewards contract on badge...'); + // Step 1: Whitelist Treasury and deployer on badge + console.log('Step 1: Whitelisting Treasury and deployer on badge...'); try { - const tx = await badge.updateWhitelistAddress(REWARDS_ADDRESS, true); - await tx.wait(); - console.log(' Rewards contract whitelisted'); + await (await badge.updateWhitelistAddress(treasuryAddress, true)).wait(); + console.log(' Treasury whitelisted on badge'); + await (await badge.updateWhitelistAddress(deployer.address, true)).wait(); + console.log(' Deployer whitelisted on badge'); } catch (error: any) { console.log(' Error or already whitelisted:', error.message?.slice(0, 50)); } - // Step 2: Whitelist Manager wallet on badge - console.log('\nStep 2: Whitelisting Manager wallet on badge...'); + // Step 2: Whitelist badge on Rewards (as ERC1155) + console.log('\nStep 2: Whitelisting badge on Rewards...'); try { - const tx = await badge.updateWhitelistAddress(deployer.address, true); - await tx.wait(); - console.log(' Manager wallet whitelisted'); + await (await rewards.whitelistToken(badgeConfig.address, 3)).wait(); // 3 = ERC1155 + console.log(' Badge whitelisted on Rewards'); } catch (error: any) { console.log(' Error or already whitelisted:', error.message?.slice(0, 50)); } - // Step 3: Mint badges to Manager + // Step 3: Mint badges to deployer console.log(`\nStep 3: Minting ${BADGES_TO_MINT} badges...`); try { const balanceBefore = await badge.balanceOf(deployer.address, badgeConfig.tokenId); @@ -101,16 +105,17 @@ async function setupBadgeReward( console.log(' Error:', error.message); } - // Step 4: Approve Rewards contract - console.log('\nStep 4: Approving Rewards contract...'); + // Step 4: Transfer badges to Treasury + console.log('\nStep 4: Transferring badges to Treasury...'); try { - const isApproved = await badge.isApprovedForAll(deployer.address, REWARDS_ADDRESS); - if (isApproved) { - console.log(' Already approved'); + const balance = await badge.balanceOf(deployer.address, badgeConfig.tokenId); + if (balance > 0n) { + await ( + await badge.safeTransferFrom(deployer.address, treasuryAddress, badgeConfig.tokenId, balance, '0x') + ).wait(); + console.log(` Transferred ${balance} badges to Treasury`); } else { - const tx = await badge.setApprovalForAll(REWARDS_ADDRESS, true); - await tx.wait(); - console.log(' Approved'); + console.log(' No badges to transfer'); } } catch (error: any) { console.log(' Error:', error.message); @@ -138,18 +143,82 @@ async function setupBadgeReward( ], }; - console.log(' Creating reward...'); + // Pre-flight diagnostics + console.log(' --- Pre-flight checks ---'); + const rewardsStateAddress = await rewards.rewardsState(); + const rewardsStateContract = await ethers.getContractAt('RewardsState', rewardsStateAddress); + const STATE_MANAGER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('STATE_MANAGER_ROLE')); + const MANAGER_ROLE = await rewards.MANAGER_ROLE(); + + const hasManagerRole = await rewards.hasRole(MANAGER_ROLE, deployer.address); + console.log(` Deployer has MANAGER_ROLE on Rewards: ${hasManagerRole}`); + + const hasStateRole = await rewardsStateContract.hasRole(STATE_MANAGER_ROLE, REWARDS_ADDRESS); + console.log(` Rewards has STATE_MANAGER_ROLE on RewardsState: ${hasStateRole}`); + + const hasDevConfigRole = await accessToken.hasRole(DEV_CONFIG_ROLE, REWARDS_ADDRESS); + console.log(` Rewards has DEV_CONFIG_ROLE on AccessToken: ${hasDevConfigRole}`); + + const isWhitelisted = await rewardsStateContract.whitelistedTokens(badgeConfig.address); + console.log(` Badge whitelisted on RewardsState: ${isWhitelisted}`); + + const treasuryBal = await badge.balanceOf(treasuryAddress, badgeConfig.tokenId); + console.log(` Treasury badge balance (tokenId ${badgeConfig.tokenId}): ${treasuryBal}`); + + const reserved = await rewardsStateContract.erc1155ReservedAmounts(badgeConfig.address, badgeConfig.tokenId); + console.log(` ERC1155 reserved: ${reserved}`); + + const totalNeeded = BigInt(1) * BigInt(REWARD_MAX_SUPPLY); // rewardAmount * maxSupply + console.log(` Total needed: ${totalNeeded}, Available: ${treasuryBal - reserved}`); + + const tokenExistsInState = await rewardsStateContract.tokenExists(badgeConfig.rewardTokenId); + console.log(` Token ${badgeConfig.rewardTokenId} exists in RewardsState: ${tokenExistsInState}`); + + console.log(' --- End pre-flight checks ---'); + + // Try staticCall first to get detailed error + console.log(' Simulating createTokenAndDepositRewards via staticCall...'); + try { + await rewards.createTokenAndDepositRewards.staticCall(rewardToken); + console.log(' staticCall succeeded, sending real tx...'); + } catch (simError: any) { + console.log(' staticCall FAILED:'); + console.log(' message:', simError.message?.slice(0, 200)); + if (simError.data) { + console.log(' raw error data:', simError.data); + // Try decoding with different contract interfaces + try { + const rewardsIface = rewards.interface; + const parsed = rewardsIface.parseError(simError.data); + console.log(' Decoded (Rewards):', parsed?.name, parsed?.args); + } catch { console.log(' Could not decode with Rewards ABI'); } + try { + const stateIface = rewardsStateContract.interface; + const parsed = stateIface.parseError(simError.data); + console.log(' Decoded (RewardsState):', parsed?.name, parsed?.args); + } catch { console.log(' Could not decode with RewardsState ABI'); } + try { + const atIface = accessToken.interface; + const parsed = atIface.parseError(simError.data); + console.log(' Decoded (AccessToken):', parsed?.name, parsed?.args); + } catch { console.log(' Could not decode with AccessToken ABI'); } + } + if (simError.revert) console.log(' revert:', simError.revert); + throw simError; // re-throw to skip actual tx + } + const tx = await rewards.createTokenAndDepositRewards(rewardToken); await tx.wait(); console.log(' Reward token created'); - // Verify badges transferred - const rewardsBalance = await badge.balanceOf(REWARDS_ADDRESS, badgeConfig.tokenId); - console.log(` Rewards contract now holds ${rewardsBalance} badges`); + // Verify badges in Treasury + const verifyBalance = await badge.balanceOf(treasuryAddress, badgeConfig.tokenId); + console.log(` Treasury now holds ${verifyBalance} badges`); } } catch (error: any) { - console.log(' Error:', error.message); + console.log(' Error:', error.message?.slice(0, 200)); if (error.reason) console.log(' Reason:', error.reason); + if (error.data) console.log(' Error data:', error.data); } } @@ -164,38 +233,32 @@ async function setupUSDCReward(rewards: any, deployer: any) { } const usdc = await ethers.getContractAt('MockUSDC', MOCK_USDC_ADDRESS); + const treasuryAddress = await rewards.treasury(); - // Step 1: Whitelist USDC token on Rewards (if function exists) - console.log('Step 1: Checking if token whitelist is required...'); + // Step 1: Whitelist USDC token on Rewards + console.log('Step 1: Whitelisting USDC on Rewards...'); try { - // Try to whitelist if function exists - const isWhitelisted = await rewards.isWhitelistedToken(MOCK_USDC_ADDRESS).catch(() => null); - if (isWhitelisted === false) { - const tx = await rewards.whitelistToken(MOCK_USDC_ADDRESS); - await tx.wait(); - console.log(' USDC whitelisted on Rewards'); - } else if (isWhitelisted === true) { - console.log(' USDC already whitelisted'); - } else { - console.log(' Token whitelist not required (function not found)'); - } + await (await rewards.whitelistToken(MOCK_USDC_ADDRESS, 1)).wait(); // 1 = ERC20 + console.log(' USDC whitelisted on Rewards'); } catch (error: any) { - console.log(' Whitelist not required or function not available'); + console.log(' Error or already whitelisted:', error.message?.slice(0, 50)); } - // Step 2: Approve and deposit USDC to treasury - console.log('\nStep 2: Approving and depositing USDC to treasury...'); + // Step 2: Mint USDC, approve Treasury, and deposit + console.log('\nStep 2: Minting, approving and depositing USDC to treasury...'); try { const totalNeeded = USDC_REWARD_AMOUNT * BigInt(REWARD_MAX_SUPPLY); - // Approve - const approveTx = await usdc.approve(REWARDS_ADDRESS, totalNeeded); - await approveTx.wait(); - console.log(` Approved ${ethers.formatUnits(totalNeeded, 6)} USDC`); + // Mint USDC to deployer + await (await usdc.mint(deployer.address, totalNeeded)).wait(); + console.log(` Minted ${ethers.formatUnits(totalNeeded, 6)} USDC`); + + // Approve Treasury (not Rewards) + await (await usdc.approve(treasuryAddress, totalNeeded)).wait(); + console.log(` Approved ${ethers.formatUnits(totalNeeded, 6)} USDC for Treasury`); // Deposit to treasury - const depositTx = await rewards.depositToTreasury(MOCK_USDC_ADDRESS, totalNeeded); - await depositTx.wait(); + await (await rewards.depositToTreasury(MOCK_USDC_ADDRESS, totalNeeded)).wait(); console.log(` Deposited ${ethers.formatUnits(totalNeeded, 6)} USDC to treasury`); } catch (error: any) { console.log(' Error:', error.message?.slice(0, 80)); @@ -223,14 +286,149 @@ async function setupUSDCReward(rewards: any, deployer: any) { ], }; + // Pre-flight diagnostics for USDC + console.log(' --- Pre-flight checks ---'); + const rewardsStateAddress = await rewards.rewardsState(); + const rewardsStateContract = await ethers.getContractAt('RewardsState', rewardsStateAddress); + const STATE_MANAGER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('STATE_MANAGER_ROLE')); + + const isWhitelisted = await rewardsStateContract.whitelistedTokens(MOCK_USDC_ADDRESS); + console.log(` USDC whitelisted on RewardsState: ${isWhitelisted}`); + + const treasuryUsdcBalance = await usdc.balanceOf(treasuryAddress); + console.log(` Treasury USDC balance: ${ethers.formatUnits(treasuryUsdcBalance, 6)}`); + + const reserved = await rewardsStateContract.reservedAmounts(MOCK_USDC_ADDRESS); + console.log(` USDC reserved: ${reserved}`); + + const totalNeeded = USDC_REWARD_AMOUNT * BigInt(REWARD_MAX_SUPPLY); + console.log(` Total USDC needed: ${ethers.formatUnits(totalNeeded, 6)}`); + console.log(` Sufficient: ${treasuryUsdcBalance >= reserved + totalNeeded}`); + + const tokenExistsInState = await rewardsStateContract.tokenExists(REWARD_TOKEN_IDS.USDC); + console.log(` Token ${REWARD_TOKEN_IDS.USDC} exists in RewardsState: ${tokenExistsInState}`); + + const hasStateRole = await rewardsStateContract.hasRole(STATE_MANAGER_ROLE, REWARDS_ADDRESS); + console.log(` Rewards has STATE_MANAGER_ROLE: ${hasStateRole}`); + + const accessTokenAddr = await rewards.getRewardTokenContract(); + const at = await ethers.getContractAt('AccessToken', accessTokenAddr); + const DEV_ROLE = await at.DEV_CONFIG_ROLE(); + const hasDevRole = await at.hasRole(DEV_ROLE, REWARDS_ADDRESS); + console.log(` Rewards has DEV_CONFIG_ROLE on AccessToken: ${hasDevRole}`); + console.log(' --- End pre-flight checks ---'); + + // Try staticCall first to get detailed error + console.log(' Simulating via staticCall...'); + try { + await rewards.createTokenAndDepositRewards.staticCall(rewardToken); + console.log(' staticCall succeeded, sending real tx...'); + } catch (simError: any) { + console.log(' staticCall FAILED:'); + console.log(' message:', simError.message?.slice(0, 200)); + if (simError.data) { + console.log(' raw error data:', simError.data); + try { + const parsed = rewards.interface.parseError(simError.data); + console.log(' Decoded (Rewards):', parsed?.name, parsed?.args); + } catch { console.log(' Could not decode with Rewards ABI'); } + try { + const parsed = rewardsStateContract.interface.parseError(simError.data); + console.log(' Decoded (RewardsState):', parsed?.name, parsed?.args); + } catch { console.log(' Could not decode with RewardsState ABI'); } + try { + const parsed = at.interface.parseError(simError.data); + console.log(' Decoded (AccessToken):', parsed?.name, parsed?.args); + } catch { console.log(' Could not decode with AccessToken ABI'); } + } + throw simError; + } + console.log(' Creating reward...'); const tx = await rewards.createTokenAndDepositRewards(rewardToken); await tx.wait(); console.log(' USDC reward token created'); - // Verify USDC transferred - const rewardsBalance = await usdc.balanceOf(REWARDS_ADDRESS); - console.log(` Rewards contract now holds ${ethers.formatUnits(rewardsBalance, 6)} USDC`); + // Verify USDC in Treasury + const finalBalance = await usdc.balanceOf(treasuryAddress); + console.log(` Treasury now holds ${ethers.formatUnits(finalBalance, 6)} USDC`); + } + } catch (error: any) { + console.log(' Error:', error.message?.slice(0, 200)); + if (error.reason) console.log(' Reason:', error.reason); + if (error.data) console.log(' Error data:', error.data); + } +} + +async function setupERC721Reward(rewards: any, deployer: any, treasuryAddress: string) { + console.log(`\n========================================`); + console.log(`Setting up ERC721 (MockERC721) Reward`); + console.log(`========================================`); + + if (!MOCK_ERC721_ADDRESS) { + console.log(' SKIPPED: MockERC721 address not provided'); + return; + } + + const mockERC721 = await ethers.getContractAt('MockERC721', MOCK_ERC721_ADDRESS); + + // Step 1: Whitelist ERC721 on Rewards + console.log('Step 1: Whitelisting MockERC721 on Rewards...'); + try { + await (await rewards.whitelistToken(MOCK_ERC721_ADDRESS, 2)).wait(); // 2 = ERC721 + console.log(' MockERC721 whitelisted'); + } catch (error: any) { + console.log(' Error or already whitelisted:', error.message?.slice(0, 50)); + } + + // Step 2: Mint ERC721 NFTs and transfer to Treasury + console.log(`\nStep 2: Minting ${ERC721_NFT_COUNT} NFTs and transferring to Treasury...`); + const mintedTokenIds: number[] = []; + try { + for (let i = 0; i < ERC721_NFT_COUNT; i++) { + await (await mockERC721.mint(deployer.address)).wait(); + // The tokenId is auto-incremented starting from 0, so we track what we mint + const tokenId = i; // MockERC721 uses _tokenIdCounter starting at 0 + await (await mockERC721.transferFrom(deployer.address, treasuryAddress, tokenId)).wait(); + mintedTokenIds.push(tokenId); + } + console.log(` Minted and transferred ${ERC721_NFT_COUNT} NFTs (tokenIds: 0-${ERC721_NFT_COUNT - 1})`); + } catch (error: any) { + console.log(' Error:', error.message?.slice(0, 80)); + console.log(` Successfully minted ${mintedTokenIds.length} NFTs before error`); + } + + // Step 3: Create ERC721 reward token + if (mintedTokenIds.length === 0) { + console.log(' SKIPPED: No NFTs were minted'); + return; + } + + // Use first REWARD_MAX_SUPPLY NFTs for the reward (1 per claim) + const rewardNftIds = mintedTokenIds.slice(0, REWARD_MAX_SUPPLY); + console.log(`\nStep 3: Creating ERC721 reward token #${REWARD_TOKEN_IDS.ERC721}...`); + try { + const exists = await rewards.isTokenExist(REWARD_TOKEN_IDS.ERC721).catch(() => false); + if (exists) { + console.log(' Reward token already exists'); + } else { + await ( + await rewards.createTokenAndDepositRewards({ + tokenId: REWARD_TOKEN_IDS.ERC721, + maxSupply: rewardNftIds.length, // 1 claim per NFT + tokenUri: `https://storage.summon.xyz/default/rewards/${REWARD_TOKEN_IDS.ERC721}/metadata.json`, + rewards: [ + { + rewardType: 2, // ERC721 + rewardAmount: 1, // 1 NFT per claim + rewardTokenAddress: MOCK_ERC721_ADDRESS, + rewardTokenIds: rewardNftIds, + rewardTokenId: 0, + }, + ], + }) + ).wait(); + console.log(` ERC721 reward token created (1 NFT per claim, ${rewardNftIds.length} max claims)`); } } catch (error: any) { console.log(' Error:', error.message); @@ -245,6 +443,8 @@ async function main() { // Get contract instances const rewards = await ethers.getContractAt('Rewards', REWARDS_ADDRESS); + const treasuryAddress = await rewards.treasury(); + console.log('Treasury address:', treasuryAddress); // Get AccessToken and grant DEV_CONFIG_ROLE if needed console.log('\n========================================'); @@ -301,6 +501,9 @@ async function main() { // Setup USDC reward await setupUSDCReward(rewards, deployer); + // Setup ERC721 reward + await setupERC721Reward(rewards, deployer, treasuryAddress); + // Summary console.log('\n========================================'); console.log('Setup Complete!'); @@ -315,14 +518,19 @@ async function main() { if (MOCK_USDC_ADDRESS) { console.log(` USDC Reward: Token ID ${REWARD_TOKEN_IDS.USDC}`); } + if (MOCK_ERC721_ADDRESS) { + console.log(` ERC721 Reward: Token ID ${REWARD_TOKEN_IDS.ERC721}`); + } console.log('\nContract Addresses:'); console.log(' Rewards:', REWARDS_ADDRESS); + console.log(' Treasury:', treasuryAddress); console.log(' KPOP Badges:', KPOP_BADGES_ADDRESS || 'Not deployed'); console.log(' F1 Badges:', F1_BADGES_ADDRESS || 'Not deployed'); console.log(' New Jeans Badges:', NEWJEANS_BADGES_ADDRESS || 'Not deployed'); console.log(' Quince Badges:', QUINCE_BADGES_ADDRESS || 'Not deployed'); console.log(' Mock USDC:', MOCK_USDC_ADDRESS || 'Not deployed'); + console.log(' Mock ERC721:', MOCK_ERC721_ADDRESS || 'Not deployed'); console.log('\n========================================'); console.log('Usage Examples'); diff --git a/scripts/upgradeRewardsSystem.ts b/scripts/upgradeRewardsSystem.ts index c9a69d4..049fcfb 100644 --- a/scripts/upgradeRewardsSystem.ts +++ b/scripts/upgradeRewardsSystem.ts @@ -26,9 +26,9 @@ import { ethers, upgrades, run } from 'hardhat'; // Default addresses (update after initial deployment) const DEFAULT_ADDRESSES = { - REWARDS_PROXY: process.env.REWARDS_PROXY_ADDRESS || '', - REWARDS_STATE: process.env.REWARDS_STATE_ADDRESS || '', - TREASURY: process.env.TREASURY_ADDRESS || '', + REWARDS_PROXY: process.env.REWARDS_PROXY_ADDRESS || '0x08809093Bd3B1d02EC55E263f4350de99557E59C', + REWARDS_STATE: process.env.REWARDS_STATE_ADDRESS || '0xf1a09e84366B68125eDEDB91535c5D39AB8E0373', + TREASURY: process.env.TREASURY_ADDRESS || '0x85974902415e87Ae6F94253648f1033163479e38', }; interface UpgradeResult { @@ -39,10 +39,7 @@ interface UpgradeResult { upgraded: boolean; } -async function upgradeContract( - contractName: string, - proxyAddress: string -): Promise { +async function upgradeContract(contractName: string, proxyAddress: string): Promise { console.log(`\nUpgrading ${contractName}...`); console.log(` Proxy: ${proxyAddress}`); @@ -55,26 +52,22 @@ async function upgradeContract( throw new Error(`Could not get implementation for ${proxyAddress}. Is this a valid proxy?`); } - // Get contract factory + // Deploy new implementation directly (bypasses OZ plugin bytecode deduplication) const ContractFactory = await ethers.getContractFactory(contractName); - - // Force import (required if not originally deployed via hardhat-upgrades) - try { - await upgrades.forceImport(proxyAddress, ContractFactory); - console.log(' Proxy registered with upgrades plugin'); - } catch (e: any) { - if (!e.message.includes('already been imported')) { - console.log(' Note:', e.message); - } - } - - // Upgrade - const upgraded = await upgrades.upgradeProxy(proxyAddress, ContractFactory, { - kind: 'uups', - }); - await upgraded.waitForDeployment(); - - // Get new implementation + console.log(' Deploying new implementation...'); + const newImplContract = await ContractFactory.deploy(); + await newImplContract.waitForDeployment(); + const newImplAddress = await newImplContract.getAddress(); + console.log(` New implementation deployed at: ${newImplAddress}`); + + // Call upgradeToAndCall on the UUPS proxy + const proxy = await ethers.getContractAt(contractName, proxyAddress); + console.log(' Calling upgradeToAndCall on proxy...'); + const tx = await proxy.upgradeToAndCall(newImplAddress, '0x'); + await tx.wait(); + console.log(` Upgrade tx confirmed: ${tx.hash}`); + + // Verify new implementation const newImplementation = await upgrades.erc1967.getImplementationAddress(proxyAddress); console.log(` New implementation: ${newImplementation}`); diff --git a/test/rewardsNftTreasury.test.ts b/test/rewardsNftTreasury.test.ts index b70f506..9a4fc17 100644 --- a/test/rewardsNftTreasury.test.ts +++ b/test/rewardsNftTreasury.test.ts @@ -590,6 +590,179 @@ describe('Rewards NFT Treasury', function () { }); + describe('withdrawAssets via Treasury', function () { + it('Should withdraw unreserved ERC20 from Treasury via withdrawAssets', async function () { + const { rewards, treasury, mockERC20, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + + const initialBalance = await mockERC20.balanceOf(user1.address); + const treasuryBalance = await mockERC20.balanceOf(treasury.target); + + // withdrawAssets for ERC20 should pull from Treasury + await rewards.connect(managerWallet).withdrawAssets( + 1, // ERC20 + user1.address, + mockERC20.target, + [], // no tokenIds for ERC20 + [treasuryBalance] // withdraw all + ); + + expect(await mockERC20.balanceOf(user1.address)).to.equal(initialBalance + treasuryBalance); + expect(await mockERC20.balanceOf(treasury.target)).to.equal(0); + }); + + it('Should withdraw unreserved ERC721 from Treasury via withdrawAssets', async function () { + const { rewards, treasury, mockERC721, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Transfer NFTs to Treasury + await mockERC721.mint(managerWallet.address); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, treasury.target, 0); + + // withdrawAssets for ERC721 should pull from Treasury + await rewards.connect(managerWallet).withdrawAssets( + 2, // ERC721 + user1.address, + mockERC721.target, + [0], // tokenIds + [] // no amounts for ERC721 + ); + + expect(await mockERC721.ownerOf(0)).to.equal(user1.address); + }); + + it('Should not withdraw reserved ERC721 via withdrawAssets', async function () { + const { rewards, treasury, mockERC721, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + + await mockERC721.mint(managerWallet.address); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, treasury.target, 0); + + // Reserve the NFT + await rewards.connect(managerWallet).createTokenAndDepositRewards({ + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [{ + rewardType: 2, + rewardAmount: 1, + rewardTokenAddress: mockERC721.target, + rewardTokenIds: [0], + rewardTokenId: 0, + }], + maxSupply: 1, + }); + + // Should revert - NFT is reserved + await expect( + rewards.connect(managerWallet).withdrawAssets( + 2, user1.address, mockERC721.target, [0], [] + ) + ).to.be.revertedWithCustomError(treasury, 'InsufficientTreasuryBalance'); + }); + + it('Should withdraw unreserved ERC1155 from Treasury via withdrawAssets', async function () { + const { rewards, treasury, mockERC1155, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + + await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, treasury.target, 1, 100, '0x'); + + // withdrawAssets for ERC1155 should pull from Treasury + await rewards.connect(managerWallet).withdrawAssets( + 3, // ERC1155 + user1.address, + mockERC1155.target, + [1], // tokenIds + [50] // amounts + ); + + expect(await mockERC1155.balanceOf(user1.address, 1)).to.equal(50); + expect(await mockERC1155.balanceOf(treasury.target, 1)).to.equal(50); + }); + + it('Should not withdraw reserved ERC1155 via withdrawAssets', async function () { + const { rewards, treasury, mockERC1155, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + + await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, treasury.target, 1, 100, '0x'); + + // Reserve 80 tokens + await rewards.connect(managerWallet).createTokenAndDepositRewards({ + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [{ + rewardType: 3, + rewardAmount: 8, + rewardTokenAddress: mockERC1155.target, + rewardTokenIds: [], + rewardTokenId: 1, + }], + maxSupply: 10, + }); + + // Try to withdraw 30 (only 20 unreserved) - should revert + await expect( + rewards.connect(managerWallet).withdrawAssets( + 3, user1.address, mockERC1155.target, [1], [30] + ) + ).to.be.revertedWithCustomError(treasury, 'InsufficientBalance'); + }); + }); + + describe('increaseRewardSupply with Treasury', function () { + it('Should check ERC20 balance on Treasury (not Rewards) when increasing supply', async function () { + const { rewards, treasury, mockERC20, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Create ERC20 reward + await rewards.connect(managerWallet).createTokenAndDepositRewards({ + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [{ + rewardType: 1, + rewardAmount: ethers.parseEther('100'), + rewardTokenAddress: mockERC20.target, + rewardTokenIds: [], + rewardTokenId: 0, + }], + maxSupply: 10, // reserves 1000 of 10000 + }); + + // Increase supply - Treasury has 9000 unreserved, enough for 90 more + await rewards.connect(managerWallet).increaseRewardSupply(1, 90); + + // Now all 10000 are reserved, so adding more should fail + await expect( + rewards.connect(managerWallet).increaseRewardSupply(1, 1) + ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + }); + + it('Should check ERC1155 balance on Treasury when increasing supply', async function () { + const { rewards, treasury, mockERC1155, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Transfer tokens to Treasury + await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, treasury.target, 1, 100, '0x'); + + // Create ERC1155 reward (reserves 50 of 100) + await rewards.connect(managerWallet).createTokenAndDepositRewards({ + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [{ + rewardType: 3, + rewardAmount: 5, + rewardTokenAddress: mockERC1155.target, + rewardTokenIds: [], + rewardTokenId: 1, + }], + maxSupply: 10, + }); + + // Increase supply by 10 more (needs 50 more, exactly matches unreserved) + await rewards.connect(managerWallet).increaseRewardSupply(1, 10); + + // Now all 100 are reserved, adding more should fail + await expect( + rewards.connect(managerWallet).increaseRewardSupply(1, 1) + ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + }); + }); + describe('Treasury Withdrawal Protection', function () { it('Should protect reserved ERC721 via withdrawERC721UnreservedTreasury', async function () { const { rewards, treasury, mockERC721, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture);