From 0f72241cb0141de9900a6f6fe299e205715f2b10 Mon Sep 17 00:00:00 2001 From: "Eugene Y. Q. Shen" Date: Wed, 25 Sep 2024 07:02:53 -0700 Subject: [PATCH] [NES-219] implement claimYield and version --- nest/src/AggregateToken.sol | 61 +++++++++++++++++++++---- nest/src/FakeComponentToken.sol | 39 +++++++++++++++- nest/src/NestStaking.sol | 6 ++- nest/src/interfaces/IComponentToken.sol | 4 ++ 4 files changed, 99 insertions(+), 11 deletions(-) diff --git a/nest/src/AggregateToken.sol b/nest/src/AggregateToken.sol index 11d6df3..3679501 100644 --- a/nest/src/AggregateToken.sol +++ b/nest/src/AggregateToken.sol @@ -43,6 +43,8 @@ contract AggregateToken is uint256 bidPrice; /// @dev URI for the AggregateToken metadata string tokenURI; + /// @dev Version of the AggregateToken + uint256 version; } // keccak256(abi.encode(uint256(keccak256("plume.storage.AggregateToken")) - 1)) & ~bytes32(uint256(0xff)) @@ -57,6 +59,8 @@ contract AggregateToken is // Constants + /// @notice Role for the admin of the AggregateToken + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); /// @notice Role for the upgrader of the AggregateToken bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); @@ -169,6 +173,13 @@ contract AggregateToken is */ error UserCurrencyTokenInsufficientBalance(IERC20 currencyToken, address user, uint256 amount); + /** + * @notice Indicates a failure because the given version is not higher than the current version + * @param invalidVersion Invalid version that is not higher than the current version + * @param version Current version of the AggregateToken + */ + error InvalidVersion(uint256 invalidVersion, uint256 version); + // Initializer /** @@ -205,6 +216,7 @@ contract AggregateToken is __UUPSUpgradeable_init(); _grantRole(DEFAULT_ADMIN_ROLE, owner); + _grantRole(ADMIN_ROLE, owner); _grantRole(UPGRADER_ROLE, owner); AggregateTokenStorage storage $ = _getAggregateTokenStorage(); @@ -279,6 +291,21 @@ contract AggregateToken is emit AggregateTokenSold(msg.sender, currencyToken, currencyTokenAmount, aggregateTokenAmount); } + /** + * @notice Claim yield for the given user + * @dev Anyone can call this function to claim yield for any user + * @param user Address of the user for which to claim yield + */ + function claimYield(address user) external returns (uint256 amount) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + IComponentToken[] storage componentTokenList = $.componentTokenList; + uint256 length = componentTokenList.length; + for (uint256 i = 0; i < length; ++i) { + amount += componentTokenList[i].unclaimedYield(user); + } + $.currencyToken.transfer(user, amount); + } + // Admin Functions /** @@ -286,7 +313,7 @@ contract AggregateToken is * @dev Only the owner can call this function, and there is no way to remove a ComponentToken later * @param componentToken ComponentToken to add */ - function addComponentToken(IComponentToken componentToken) external onlyRole(DEFAULT_ADMIN_ROLE) { + function addComponentToken(IComponentToken componentToken) external onlyRole(ADMIN_ROLE) { AggregateTokenStorage storage $ = _getAggregateTokenStorage(); if ($.componentTokenMap[componentToken]) { revert ComponentTokenAlreadyListed(componentToken); @@ -306,7 +333,7 @@ contract AggregateToken is function buyComponentToken( IComponentToken componentToken, uint256 currencyTokenAmount - ) public onlyRole(DEFAULT_ADMIN_ROLE) { + ) public onlyRole(ADMIN_ROLE) { AggregateTokenStorage storage $ = _getAggregateTokenStorage(); if (!$.componentTokenMap[componentToken]) { @@ -333,7 +360,7 @@ contract AggregateToken is function sellComponentToken( IComponentToken componentToken, uint256 currencyTokenAmount - ) public onlyRole(DEFAULT_ADMIN_ROLE) { + ) public onlyRole(ADMIN_ROLE) { IERC20 currencyToken = _getAggregateTokenStorage().currencyToken; uint256 componentTokenAmount = componentToken.sell(currencyToken, currencyTokenAmount); @@ -343,12 +370,25 @@ contract AggregateToken is // Admin Setter Functions + /** + * @notice Set the version of the AggregateToken + * @dev Only the owner can call this setter + * @param version New version of the AggregateToken + */ + function setVersion(uint256 version) external onlyRole(ADMIN_ROLE) { + AggregateTokenStorage storage $ = _getAggregateTokenStorage(); + if (version <= $.version) { + revert InvalidVersion(version, $.version); + } + $.version = version; + } + /** * @notice Set the CurrencyToken used to mint and burn the AggregateToken * @dev Only the owner can call this setter * @param currencyToken New CurrencyToken */ - function setCurrencyToken(IERC20 currencyToken) external onlyRole(DEFAULT_ADMIN_ROLE) { + function setCurrencyToken(IERC20 currencyToken) external onlyRole(ADMIN_ROLE) { _getAggregateTokenStorage().currencyToken = currencyToken; } @@ -357,7 +397,7 @@ contract AggregateToken is * @dev Only the owner can call this setter * @param askPrice New ask price */ - function setAskPrice(uint256 askPrice) external onlyRole(DEFAULT_ADMIN_ROLE) { + function setAskPrice(uint256 askPrice) external onlyRole(ADMIN_ROLE) { _getAggregateTokenStorage().askPrice = askPrice; } @@ -366,7 +406,7 @@ contract AggregateToken is * @dev Only the owner can call this setter * @param bidPrice New bid price */ - function setBidPrice(uint256 bidPrice) external onlyRole(DEFAULT_ADMIN_ROLE) { + function setBidPrice(uint256 bidPrice) external onlyRole(ADMIN_ROLE) { _getAggregateTokenStorage().bidPrice = bidPrice; } @@ -375,12 +415,17 @@ contract AggregateToken is * @dev Only the owner can call this setter * @param tokenURI New token URI */ - function setTokenURI(string memory tokenURI) external onlyRole(DEFAULT_ADMIN_ROLE) { + function setTokenURI(string memory tokenURI) external onlyRole(ADMIN_ROLE) { _getAggregateTokenStorage().tokenURI = tokenURI; } // Getter View Functions + /// @notice Version of the AggregateToken + function getVersion() external view returns (uint256) { + return _getAggregateTokenStorage().version; + } + /// @notice CurrencyToken used to mint and burn the AggregateToken function getCurrencyToken() external view returns (IERC20) { return _getAggregateTokenStorage().currencyToken; @@ -469,7 +514,7 @@ contract AggregateToken is * @param user Address of the user for which to get the unclaimed yield * @return amount Amount of yield that the user has not yet claimed */ - function unclaimedYield(address user) external view returns (uint256 amount) { + function unclaimedYield(address user) public view returns (uint256 amount) { return totalYield(user) - claimedYield(user); } diff --git a/nest/src/FakeComponentToken.sol b/nest/src/FakeComponentToken.sol index 85efb09..27d08ac 100644 --- a/nest/src/FakeComponentToken.sol +++ b/nest/src/FakeComponentToken.sol @@ -31,6 +31,8 @@ contract FakeComponentToken is IERC20 currencyToken; /// @dev Number of decimals of the FakeComponentToken uint8 decimals; + /// @dev Version of the FakeComponentToken + uint256 version; } // keccak256(abi.encode(uint256(keccak256("plume.storage.FakeComponentToken")) - 1)) & ~bytes32(uint256(0xff)) @@ -74,6 +76,13 @@ contract FakeComponentToken is // Errors + /** + * @notice Indicates a failure because the given version is not higher than the current version + * @param invalidVersion Invalid version that is not higher than the current version + * @param version Current version of the FakeComponentToken + */ + error InvalidVersion(uint256 invalidVersion, uint256 version); + /** * @notice Indicates a failure because the given CurrencyToken does not match actual CurrencyToken * @param invalidCurrencyToken CurrencyToken that does not match the actual CurrencyToken @@ -187,8 +196,31 @@ contract FakeComponentToken is componentTokenAmount = amount; } + /** + * @notice Claim yield for the given user + * @dev Anyone can call this function to claim yield for any user + * @param user Address of the user for which to claim yield + */ + function claimYield(address user) external returns (uint256 amount) { + amount = unclaimedYield(user); + _getFakeComponentTokenStorage().currencyToken.transfer(user, amount); + } + // Admin Setter Functions + /** + * @notice Set the version of the FakeComponentToken + * @dev Only the owner can call this setter + * @param version New version of the FakeComponentToken + */ + function setVersion(uint256 version) external onlyRole(DEFAULT_ADMIN_ROLE) { + FakeComponentTokenStorage storage $ = _getFakeComponentTokenStorage(); + if (version <= $.version) { + revert InvalidVersion(version, $.version); + } + $.version = version; + } + /** * @notice Set the CurrencyToken used to mint and burn the FakeComponentToken * @dev Only the owner can call this setter @@ -200,6 +232,11 @@ contract FakeComponentToken is // Getter View Functions + /// @notice Version of the FakeComponentToken + function getVersion() external view returns (uint256) { + return _getFakeComponentTokenStorage().version; + } + /// @notice CurrencyToken used to mint and burn the FakeComponentToken function getCurrencyToken() external view returns (IERC20) { return _getFakeComponentTokenStorage().currencyToken; @@ -243,7 +280,7 @@ contract FakeComponentToken is * @param user Address of the user for which to get the unclaimed yield * @return amount Amount of yield that the user has not yet claimed */ - function unclaimedYield(address user) external view returns (uint256 amount) { + function unclaimedYield(address user) public view returns (uint256 amount) { return totalYield(user) - claimedYield(user); } diff --git a/nest/src/NestStaking.sol b/nest/src/NestStaking.sol index a910e44..81c7a17 100644 --- a/nest/src/NestStaking.sol +++ b/nest/src/NestStaking.sol @@ -39,6 +39,8 @@ contract NestStaking is Initializable, AccessControlUpgradeable, UUPSUpgradeable // Constants + /// @notice Role for the admin of the Nest Staking protocol + bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); /// @notice Role for the upgrader of the Nest Staking protocol bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); @@ -114,7 +116,7 @@ contract NestStaking is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @dev Only the owner can call this function * @param aggregateToken AggregateToken to be featured */ - function featureToken(IAggregateToken aggregateToken) external onlyRole(DEFAULT_ADMIN_ROLE) { + function featureToken(IAggregateToken aggregateToken) external onlyRole(ADMIN_ROLE) { NestStakingStorage storage $ = _getNestStakingStorage(); if ($.isFeatured[aggregateToken]) { revert TokenAlreadyFeatured(aggregateToken); @@ -129,7 +131,7 @@ contract NestStaking is Initializable, AccessControlUpgradeable, UUPSUpgradeable * @dev Only the owner can call this function * @param aggregateToken AggregateToken to be unfeatured */ - function unfeatureToken(IAggregateToken aggregateToken) external onlyRole(DEFAULT_ADMIN_ROLE) { + function unfeatureToken(IAggregateToken aggregateToken) external onlyRole(ADMIN_ROLE) { NestStakingStorage storage $ = _getNestStakingStorage(); if (!$.isFeatured[aggregateToken]) { revert TokenNotFeatured(aggregateToken); diff --git a/nest/src/interfaces/IComponentToken.sol b/nest/src/interfaces/IComponentToken.sol index 3d092e6..bb98042 100644 --- a/nest/src/interfaces/IComponentToken.sol +++ b/nest/src/interfaces/IComponentToken.sol @@ -7,6 +7,10 @@ interface IComponentToken is IERC20 { function buy(IERC20 currencyToken, uint256 currencyTokenAmount) external returns (uint256 componentTokenAmount); function sell(IERC20 currencyToken, uint256 currencyTokenAmount) external returns (uint256 componentTokenAmount); + function claimYield(address user) external returns (uint256 amount); + + function getVersion() external view returns (uint256 version); + function getCurrencyToken() external view returns (IERC20 currencyToken); function totalYield() external view returns (uint256 amount); function claimedYield() external view returns (uint256 amount); function unclaimedYield() external view returns (uint256 amount);