From 0a8363526cf11a78b046157cb02d1570e279be0a Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Wed, 17 Dec 2025 12:19:14 +0800 Subject: [PATCH 01/20] Staking Mod --- lib/forge-std | 2 +- src/token/Staking/StakingFacet.sol | 92 +++++ src/token/Staking/StakingMod.sol | 521 +++++++++++++++++++++++++++++ 3 files changed, 614 insertions(+), 1 deletion(-) create mode 100644 src/token/Staking/StakingFacet.sol create mode 100644 src/token/Staking/StakingMod.sol diff --git a/lib/forge-std b/lib/forge-std index 8e40513d..27ba11c8 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 8e40513d678f392f398620b3ef2b418648b33e89 +Subproject commit 27ba11c86ac93d8d4a50437ae26621468fe63c20 diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol new file mode 100644 index 00000000..59c6a71c --- /dev/null +++ b/src/token/Staking/StakingFacet.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/** + * @title LibStaking - Standard Staking Library + * @notice Provides internal functions and storage layout for staking ERC-20, ERC-721, and ERC-1155 tokens. + * @dev Uses ERC-8042 for storage location standardization. + */ + +/** + * @notice Thrown when attempting to stake an unsupported token type. + * @param tokenAddress The address of the unsupported token. + */ +error StakingUnsupportedToken(address tokenAddress); + +/** + * @notice Thrown when attempting to stake an amount below the minimum stake amount. + * @param amount The attempted stake amount. + * @param minAmount The minimum required stake amount. + */ +error StakingAmountBelowMinimum(uint256 amount, uint256 minAmount); + +/** + * @notice Thrown when attempting to stake an amount above the maximum stake amount. + * @param amount The attempted stake amount. + * @param maxAmount The maximum allowed stake amount. + */ +error StakingAmountAboveMaximum(uint256 amount, uint256 maxAmount); + +/** + * @notice Thrown when attempting to unstake before the cooldown period has elapsed. + * @param stakedAt The timestamp when the tokens were staked. + * @param cooldownPeriod The required cooldown period in seconds. + * @param currentTime The current block timestamp. + */ +error StakingCooldownNotElapsed(uint256 stakedAt, uint256 cooldownPeriod, uint256 currentTime); + +bytes32 constant STAKING_STORAGE_POSITION = keccak256("compose.staking"); + +/** + * @notice Structure containing staking information for a specific token. + * @param amount The amount of tokens staked. + * @param stakedAt The timestamp when the tokens were staked. + * @param lastClaimedAt The timestamp when rewards were last claimed. + * @param accumulatedRewards The total accumulated rewards for the staked tokens. + */ +struct StakedTokenInfo { + uint256 amount; + uint256 stakedAt; + uint256 lastClaimedAt; + uint256 accumulatedRewards; +} + +/** + * @notice Structure containing type of tokens being staked. + * @param isERC20 Boolean indicating if the token is ERC-20. + * @param isERC721 Boolean indicating if the token is ERC-721. + * @param isERC1155 Boolean indicating if the token is ERC-1155. + */ +struct TokenType { + bool isERC20; + bool isERC721; + bool isERC1155; +} + +/** + * @custom:storage-location erc8042:compose.staking + */ +struct StakingStorage { + mapping(address tokenType => TokenType) supportedTokens; + mapping(address tokenAddress => mapping(uint256 tokenId => StakedTokenInfo)) stakedTokens; + uint256 baseAPR; + uint256 rewardDecayRate; + uint256 compoundFrequency; + address rewardToken; + mapping(address user => uint256 totalStaked) totalStakedPerToken; + uint256 cooldownPeriod; + uint256 maxStakeAmount; + uint256 minStakeAmount; +} + +/** + * @notice Returns the staking storage structure from its predefined slot. + * @dev Uses inline assembly to access diamond storage location. + * @return s The storage reference to StakingStorage. + */ +function getStorage() pure returns (StakingStorage storage s) { + bytes32 position = STAKING_STORAGE_POSITION; + assembly { + s.slot := position + } +} diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol new file mode 100644 index 00000000..9e498aa6 --- /dev/null +++ b/src/token/Staking/StakingMod.sol @@ -0,0 +1,521 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +/** + * @dev Simplified ERC20 interface. + */ +interface IERC20 { + function totalSupply() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function transfer(address to, uint256 amount) external returns (bool); + + function allowance(address owner, address spender) external view returns (uint256); + + function approve(address spender, uint256 amount) external returns (bool); + + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} + +/** + * @dev Simplified ERC721 interface. + */ +interface IERC721 { + function ownerOf(uint256 tokenId) external view returns (address owner); + + function safeTransferFrom(address from, address to, uint256 tokenId) external; +} + +/** + * @dev Simplified ERC1155 interface. + */ +interface IERC1155 { + function balanceOf(address account, uint256 id) external view returns (uint256); + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; +} + +/** + * @title ERC-721 Token Receiver Interface + * @notice Interface for contracts that want to handle safe transfers of ERC-721 tokens. + * @dev Contracts implementing this must return the selector to confirm token receipt. + */ +interface IERC721Receiver { + /** + * @notice Handles the receipt of an NFT. + * @param _operator The address which called `safeTransferFrom`. + * @param _from The previous owner of the token. + * @param _tokenId The NFT identifier being transferred. + * @param _data Additional data with no specified format. + * @return The selector to confirm the token transfer. + */ + function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) + external + returns (bytes4); +} + +/** + * @title ERC-1155 Token Receiver Interface + * @notice Interface that must be implemented by smart contracts in order to receive ERC-1155 token transfers. + */ +interface IERC1155Receiver { + /** + * @notice Handles the receipt of a single ERC-1155 token type. + * @dev This function is called at the end of a `safeTransferFrom` after the balance has been updated. + * + * IMPORTANT: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param _operator The address which initiated the transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _id The ID of the token being transferred. + * @param _value The amount of tokens being transferred. + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed. + */ + function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) + external + returns (bytes4); + + /** + * @notice Handles the receipt of multiple ERC-1155 token types. + * @dev This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. + * + * IMPORTANT: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param _operator The address which initiated the batch transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _ids An array containing ids of each token being transferred (order and length must match _values array). + * @param _values An array containing amounts of each token being transferred (order and length must match _ids array). + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed. + */ + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external returns (bytes4); +} + +contract StakingFacet { + /** + * @title LibStaking - Standard Staking Library + * @notice Provides internal functions and storage layout for staking ERC-20, ERC-721, and ERC-1155 tokens. + * @dev Uses ERC-8042 for storage location standardization. + */ + /** + * @notice Thrown when attempting to stake an unsupported token type. + * @param tokenAddress The address of the unsupported token. + */ + error StakingUnsupportedToken(address tokenAddress); + + /** + * @notice Thrown when attempting to stake a zero amount. + */ + error StakingZeroStakeAmount(); + + /** + * @notice Thrown when attempting to stake an amount below the minimum stake amount. + * @param amount The attempted stake amount. + * @param minAmount The minimum required stake amount. + */ + error StakingAmountBelowMinimum(uint256 amount, uint256 minAmount); + + /** + * @notice Thrown when attempting to stake an amount above the maximum stake amount. + * @param amount The attempted stake amount. + * @param maxAmount The maximum allowed stake amount. + */ + error StakingAmountAboveMaximum(uint256 amount, uint256 maxAmount); + + /** + * @notice Thrown when there's no rewards to claim for the staked token. + * @param tokenAddress The address of the staked token. + * @param tokenId The ID of the staked token. + */ + error StakingNoRewardsToClaim(address tokenAddress, uint256 tokenId); + + /** + * @notice Thrown when attempting to unstake before the cooldown period has elapsed. + * @param stakedAt The timestamp when the tokens were staked. + * @param cooldownPeriod The required cooldown period in seconds. + * @param currentTime The current block timestamp. + */ + error StakingCooldownNotElapsed(uint256 stakedAt, uint256 cooldownPeriod, uint256 currentTime); + + /** + * @notice Thrown when owner is not the owner of the staked token. + * @param owner The address of the token owner. + * @param tokenAddress The address of the staked token. + * @param tokenId The ID of the staked token. + */ + error StakingNotTokenOwner(address owner, address tokenAddress, uint256 tokenId); + + /** + * @notice Thrown when an account has insufficient balance for a transfer or burn. + * @param _sender Address attempting the transfer. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + */ + error StakingInsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + + /** + * @notice Thrown when the sender address is invalid (e.g., zero address). + * @param _sender Invalid sender address. + */ + error StakingInvalidSender(address _sender); + + /** + * @notice Thrown when the receiver address is invalid (e.g., zero address). + * @param _receiver Invalid receiver address. + */ + error StakingInvalidReceiver(address _receiver); + + /** + * @notice Thrown when a spender tries to use more than the approved allowance. + * @param _spender Address attempting to spend. + * @param _allowance Current allowance for the spender. + * @param _needed Amount required to complete the operation. + */ + error StakingInsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + + /** + * @notice Emitted when tokens are successfully staked. + * @param staker The address of the user who staked the tokens. + * @param tokenAddress The address of the staked token. + * @param tokenId The ID of the staked token. + * @param amount The amount of tokens staked. + */ + event TokensStaked(address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 amount); + + /** + * @notice Emitted when tokens are successfully unstaked. + * @param staker The address of the user who unstaked the tokens. + * @param tokenAddress The address of the unstaked token. + * @param tokenId The ID of the unstaked token. + * @param amount The amount of tokens unstaked. + */ + event TokensUnstaked(address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 amount); + + /** + * @notice Emitted when rewards are claimed for staked tokens. + * @param staker The address of the user who claimed the rewards. + * @param tokenAddress The address of the staked token. + * @param tokenId The ID of the staked token. + * @param rewardAmount The amount of rewards claimed. + */ + event RewardsClaimed( + address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 rewardAmount + ); + + /** + * @notice Emitted when an approval is made for a spender by an owner. + * @param _owner The address granting the allowance. + * @param _spender The address receiving the allowance. + * @param _oldValue The previous allowance amount. + * @param _newValue The new allowance amount. + */ + event Approval(address indexed _owner, address indexed _spender, uint256 _oldValue, uint256 _newValue); + + /** + * @notice Emitted when tokens are transferred between two addresses. + * @param _from Address sending the tokens. + * @param _to Address receiving the tokens. + * @param _value Amount of tokens transferred. + */ + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + bytes32 constant STAKING_STORAGE_POSITION = keccak256("compose.staking"); + + /** + * @notice Structure containing staking information for a specific token. + * @param amount The amount of tokens staked. + * @param stakedAt The timestamp when the tokens were staked. + * @param lastClaimedAt The timestamp when rewards were last claimed. + * @param accumulatedRewards The total accumulated rewards for the staked tokens. + */ + struct StakedTokenInfo { + uint256 amount; + uint256 stakedAt; + uint256 lastClaimedAt; + uint256 accumulatedRewards; + } + + /** + * @notice Structure containing type of tokens being staked. + * @param isERC20 Boolean indicating if the token is ERC-20. + * @param isERC721 Boolean indicating if the token is ERC-721. + * @param isERC1155 Boolean indicating if the token is ERC-1155. + */ + struct TokenType { + bool isERC20; + bool isERC721; + bool isERC1155; + } + + /** + * @custom:storage-location erc8042:compose.staking + */ + struct StakingStorage { + mapping(address tokenType => TokenType) supportedTokens; + mapping(address tokenAddress => mapping(uint256 tokenId => StakedTokenInfo)) stakedTokens; + uint256 baseAPR; + uint256 rewardDecayRate; + uint256 compoundFrequency; + address rewardToken; + mapping(address tokenAddress => uint256 totalStaked) totalStakedPerToken; + uint256 cooldownPeriod; + uint256 maxStakeAmount; + uint256 minStakeAmount; + mapping(address user => mapping(address spender => uint256 allowance)) allowance; + } + + /** + * @notice Returns the staking storage structure from its predefined slot. + * @dev Uses inline assembly to access diamond storage location. + * @return s The storage reference to StakingStorage. + */ + function getStorage() internal pure returns (StakingStorage storage s) { + bytes32 position = STAKING_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Stakes tokens of a supported type. + * @param _tokenAddress The address of the token to stake. + * @param _tokenId The ID of the token to stake (for ERC-721 and ERC-1155). + * @param _amount The amount of tokens to stake. + */ + function stakeToken(address _tokenAddress, uint256 _tokenId, uint256 _amount) external { + StakingStorage storage s = getStorage(); + + TokenType storage tokenType = s.supportedTokens[_tokenAddress]; + if (!tokenType.isERC20 && !tokenType.isERC721 && !tokenType.isERC1155) { + revert StakingUnsupportedToken(_tokenAddress); + } + if (_amount < s.minStakeAmount) { + revert StakingAmountBelowMinimum(_amount, s.minStakeAmount); + } + if (_amount > s.maxStakeAmount) { + revert StakingAmountAboveMaximum(_amount, s.maxStakeAmount); + } + + if (s.supportedTokens[_tokenAddress].isERC20) { + IERC20(_tokenAddress).transferFrom(msg.sender, address(this), _amount); + _stakeERC20(_tokenAddress, _amount); + } else if (s.supportedTokens[_tokenAddress].isERC721) { + IERC721(_tokenAddress).safeTransferFrom(msg.sender, address(this), _tokenId); + _stakeERC721(_tokenAddress, _tokenId); + } else if (s.supportedTokens[_tokenAddress].isERC1155) { + IERC1155(_tokenAddress).safeTransferFrom(msg.sender, address(this), _tokenId, _amount, ""); + _stakeERC1155(_tokenAddress, _tokenId, _amount); + } + } + + /** + * @notice Unstakes tokens of a supported type. + * @param _tokenAddress The address of the token to unstake. + * @param _tokenId The ID of the token to unstake (for ERC-721 and ERC-1155). + */ + function unstakeToken(address _tokenAddress, uint256 _tokenId) external { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + uint256 amount = stake.amount; + if (amount == 0) { + revert StakingZeroStakeAmount(); + } + + if (s.cooldownPeriod > 0 && block.timestamp <= stake.stakedAt + s.cooldownPeriod) { + revert StakingCooldownNotElapsed(stake.stakedAt, s.cooldownPeriod, block.timestamp); + } + + uint256 rewards = calculateRewards(_tokenAddress, _tokenId); + if (rewards > 0) { + _claimRewards(_tokenAddress, _tokenId); + } + + if (s.supportedTokens[_tokenAddress].isERC20) { + IERC20(_tokenAddress).transfer(msg.sender, amount); + } else if (s.supportedTokens[_tokenAddress].isERC721) { + IERC721(_tokenAddress).safeTransferFrom(address(this), msg.sender, _tokenId); + } else if (s.supportedTokens[_tokenAddress].isERC1155) { + IERC1155(_tokenAddress).safeTransferFrom(address(this), msg.sender, _tokenId, amount, ""); + } + + s.totalStakedPerToken[_tokenAddress] -= amount; + delete s.stakedTokens[_tokenAddress][_tokenId]; + + emit TokensUnstaked(msg.sender, _tokenAddress, _tokenId, amount); + } + + /** + * @notice An admin function to support a new token type for staking. + * @param _tokenAddress The address of the token to support. + * @param _isERC20 Boolean indicating if the token is ERC-20. + * @param _isERC721 Boolean indicating if the token is ERC-721. + * @param _isERC1155 Boolean indicating if the token is ERC-1155 + * @dev This function should be restricted to admin use only. + */ + function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) external { + StakingStorage storage s = getStorage(); + s.supportedTokens[_tokenAddress] = TokenType({isERC20: _isERC20, isERC721: _isERC721, isERC1155: _isERC1155}); + } + + /** + * @notice An admin function to set staking parameters. + * @param _baseAPR The base annual percentage rate for rewards. + * @param _rewardDecayRate The decay rate for rewards over time. + * @param _compoundFrequency The frequency at which rewards are compounded. + * @param _rewardToken The address of the token used for rewards. + * @param _cooldownPeriod The cooldown period before unstaking is allowed. + * @param _minStakeAmount The minimum amount required to stake. + * @param _maxStakeAmount The maximum amount allowed to stake. + * @dev This function should be restricted to admin use only. + */ + function setStakingParameters( + uint256 _baseAPR, + uint256 _rewardDecayRate, + uint256 _compoundFrequency, + address _rewardToken, + uint256 _cooldownPeriod, + uint256 _minStakeAmount, + uint256 _maxStakeAmount + ) external { + StakingStorage storage s = getStorage(); + s.baseAPR = _baseAPR; + s.rewardDecayRate = _rewardDecayRate; + s.compoundFrequency = _compoundFrequency; + s.rewardToken = _rewardToken; + s.cooldownPeriod = _cooldownPeriod; + s.minStakeAmount = _minStakeAmount; + s.maxStakeAmount = _maxStakeAmount; + } + + /** + * @notice Claims rewards for a staked token. + * @dev Updates the last claimed timestamp and accumulated rewards. + * @param _tokenAddress The address of the staked token. + * @param _tokenId The ID of the staked token. + */ + function claimRewards(address _tokenAddress, uint256 _tokenId) external { + _claimRewards(_tokenAddress, _tokenId); + } + + /** + * @notice Stake ERC-20 tokens + * @dev Transfers token from the user and updates the amount staked and staking info. + * @param _tokenAddress The address of the ERC-20 token to stake. + * @param _value The amount of tokens to stake. + */ + function _stakeERC20(address _tokenAddress, uint256 _value) internal { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][0]; + + stake.amount += _value; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; + + s.totalStakedPerToken[_tokenAddress] += _value; + } + + /** + * @notice Stake ERC-721 tokens + * @dev Transfers token from the user and updates the amount staked and staking info. + * @param _tokenAddress The address of the ERC-721 token to stake. + * @param _tokenId The ID of the token to stake. + */ + function _stakeERC721(address _tokenAddress, uint256 _tokenId) internal { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + stake.amount = 1; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; + + s.totalStakedPerToken[_tokenAddress] += 1; + } + + /** + * @notice Stake ERC-1155 tokens + * @dev Transfers token from the user and updates the amount staked and staking info. + * @param _tokenAddress The address of the ERC-1155 token to stake. + * @param _tokenId The ID of the token to stake. + * @param _value The amount of tokens to stake. + */ + function _stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) internal { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + stake.amount += _value; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; + + s.totalStakedPerToken[_tokenAddress] += _value; + } + + /** + * @notice Calculates the rewards for a staked token. + * @dev Uses base APR, decay rate, and compounding frequency to compute rewards. + * @param _tokenAddress The address of the staked token. + * @param _tokenId The ID of the staked token. + * @return finalReward The calculated reward amount. + */ + function calculateRewards(address _tokenAddress, uint256 _tokenId) internal view returns (uint256) { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + uint256 stakedDuration = block.timestamp - stake.lastClaimedAt; + if (stakedDuration == 0 || stake.amount == 0) { + return 0; + } + + uint256 baseReward = (stake.amount * s.baseAPR * stakedDuration) / (365 days * 100); + + uint256 decayFactor = + s.rewardDecayRate > 0 ? (s.rewardDecayRate ** (stakedDuration / s.compoundFrequency)) : 10 ** 18; + + uint256 finalReward = (baseReward * decayFactor) / (10 ** 18); + + return finalReward; + } + + /** + * @notice Claimes rewards for a staked token. + * @dev Internal function to update staking info after rewards are claimed. + * @param _tokenAddress The address of the staked token. + * @param _tokenId The ID of the staked token. + */ + function _claimRewards(address _tokenAddress, uint256 _tokenId) internal { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + uint256 rewards = calculateRewards(_tokenAddress, _tokenId); + if (rewards == 0) { + return; + } + + IERC20(s.rewardToken).transfer(msg.sender, rewards); + + stake.lastClaimedAt = block.timestamp; + stake.accumulatedRewards += rewards; + + emit RewardsClaimed(msg.sender, _tokenAddress, _tokenId, rewards); + } + + /** + * @notice Support Interface to satisfy ERC-165 standard. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the contract implements the requested interface. + */ + function supportsInterface(bytes4 interfaceId) external pure returns (bool) { + return interfaceId == type(IERC721Receiver).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId; + } +} From 998efcef4b55b99065e09aacedc0dfcd29811e66 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Wed, 17 Dec 2025 15:40:01 +0800 Subject: [PATCH 02/20] Staking Facet and revise Staking Mod --- src/token/Staking/StakingFacet.sol | 560 ++++++++++++++++++++++++---- src/token/Staking/StakingMod.sol | 579 +++++------------------------ 2 files changed, 581 insertions(+), 558 deletions(-) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index 59c6a71c..e347f6ed 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -2,91 +2,525 @@ pragma solidity >=0.8.30; /** - * @title LibStaking - Standard Staking Library - * @notice Provides internal functions and storage layout for staking ERC-20, ERC-721, and ERC-1155 tokens. - * @dev Uses ERC-8042 for storage location standardization. + * @dev Simplified ERC20 interface. */ +interface IERC20 { + function totalSupply() external view returns (uint256); -/** - * @notice Thrown when attempting to stake an unsupported token type. - * @param tokenAddress The address of the unsupported token. - */ -error StakingUnsupportedToken(address tokenAddress); + function balanceOf(address account) external view returns (uint256); -/** - * @notice Thrown when attempting to stake an amount below the minimum stake amount. - * @param amount The attempted stake amount. - * @param minAmount The minimum required stake amount. - */ -error StakingAmountBelowMinimum(uint256 amount, uint256 minAmount); + function transfer(address to, uint256 amount) external returns (bool); -/** - * @notice Thrown when attempting to stake an amount above the maximum stake amount. - * @param amount The attempted stake amount. - * @param maxAmount The maximum allowed stake amount. - */ -error StakingAmountAboveMaximum(uint256 amount, uint256 maxAmount); + function allowance(address owner, address spender) external view returns (uint256); + + function approve(address spender, uint256 amount) external returns (bool); + + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} /** - * @notice Thrown when attempting to unstake before the cooldown period has elapsed. - * @param stakedAt The timestamp when the tokens were staked. - * @param cooldownPeriod The required cooldown period in seconds. - * @param currentTime The current block timestamp. + * @dev Simplified ERC721 interface. */ -error StakingCooldownNotElapsed(uint256 stakedAt, uint256 cooldownPeriod, uint256 currentTime); +interface IERC721 { + function ownerOf(uint256 tokenId) external view returns (address owner); -bytes32 constant STAKING_STORAGE_POSITION = keccak256("compose.staking"); + function safeTransferFrom(address from, address to, uint256 tokenId) external; +} /** - * @notice Structure containing staking information for a specific token. - * @param amount The amount of tokens staked. - * @param stakedAt The timestamp when the tokens were staked. - * @param lastClaimedAt The timestamp when rewards were last claimed. - * @param accumulatedRewards The total accumulated rewards for the staked tokens. + * @dev Simplified ERC1155 interface. */ -struct StakedTokenInfo { - uint256 amount; - uint256 stakedAt; - uint256 lastClaimedAt; - uint256 accumulatedRewards; +interface IERC1155 { + function balanceOf(address account, uint256 id) external view returns (uint256); + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; } /** - * @notice Structure containing type of tokens being staked. - * @param isERC20 Boolean indicating if the token is ERC-20. - * @param isERC721 Boolean indicating if the token is ERC-721. - * @param isERC1155 Boolean indicating if the token is ERC-1155. + * @title ERC-721 Token Receiver Interface + * @notice Interface for contracts that want to handle safe transfers of ERC-721 tokens. + * @dev Contracts implementing this must return the selector to confirm token receipt. */ -struct TokenType { - bool isERC20; - bool isERC721; - bool isERC1155; +interface IERC721Receiver { + /** + * @notice Handles the receipt of an NFT. + * @param _operator The address which called `safeTransferFrom`. + * @param _from The previous owner of the token. + * @param _tokenId The NFT identifier being transferred. + * @param _data Additional data with no specified format. + * @return The selector to confirm the token transfer. + */ + function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) + external + returns (bytes4); } /** - * @custom:storage-location erc8042:compose.staking + * @title ERC-1155 Token Receiver Interface + * @notice Interface that must be implemented by smart contracts in order to receive ERC-1155 token transfers. */ -struct StakingStorage { - mapping(address tokenType => TokenType) supportedTokens; - mapping(address tokenAddress => mapping(uint256 tokenId => StakedTokenInfo)) stakedTokens; - uint256 baseAPR; - uint256 rewardDecayRate; - uint256 compoundFrequency; - address rewardToken; - mapping(address user => uint256 totalStaked) totalStakedPerToken; - uint256 cooldownPeriod; - uint256 maxStakeAmount; - uint256 minStakeAmount; +interface IERC1155Receiver { + /** + * @notice Handles the receipt of a single ERC-1155 token type. + * @dev This function is called at the end of a `safeTransferFrom` after the balance has been updated. + * + * IMPORTANT: To accept the transfer, this must return + * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` + * (i.e. 0xf23a6e61, or its own function selector). + * + * @param _operator The address which initiated the transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _id The ID of the token being transferred. + * @param _value The amount of tokens being transferred. + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed. + */ + function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) + external + returns (bytes4); + + /** + * @notice Handles the receipt of multiple ERC-1155 token types. + * @dev This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. + * + * IMPORTANT: To accept the transfer(s), this must return + * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` + * (i.e. 0xbc197c81, or its own function selector). + * + * @param _operator The address which initiated the batch transfer (i.e. msg.sender). + * @param _from The address which previously owned the token. + * @param _ids An array containing ids of each token being transferred (order and length must match _values array). + * @param _values An array containing amounts of each token being transferred (order and length must match _ids array). + * @param _data Additional data with no specified format. + * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed. + */ + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _values, + bytes calldata _data + ) external returns (bytes4); } /** - * @notice Returns the staking storage structure from its predefined slot. - * @dev Uses inline assembly to access diamond storage location. - * @return s The storage reference to StakingStorage. + * @title Staking Facet + * @notice A complete, dependency-free staking facet for ERC-20, ERC-721, and ERC-1155 tokens. + * @dev Implements staking, unstaking, and reward claiming functionalities using diamond storage. */ -function getStorage() pure returns (StakingStorage storage s) { - bytes32 position = STAKING_STORAGE_POSITION; - assembly { - s.slot := position +contract StakingFacet { + /** + * @title LibStaking - Standard Staking Library + * @notice Provides internal functions and storage layout for staking ERC-20, ERC-721, and ERC-1155 tokens. + * @dev Uses ERC-8042 for storage location standardization. + */ + /** + * @notice Thrown when attempting to stake an unsupported token type. + * @param tokenAddress The address of the unsupported token. + */ + error StakingUnsupportedToken(address tokenAddress); + + /** + * @notice Thrown when attempting to stake a zero amount. + */ + error StakingZeroStakeAmount(); + + /** + * @notice Thrown when attempting to stake an amount below the minimum stake amount. + * @param amount The attempted stake amount. + * @param minAmount The minimum required stake amount. + */ + error StakingAmountBelowMinimum(uint256 amount, uint256 minAmount); + + /** + * @notice Thrown when attempting to stake an amount above the maximum stake amount. + * @param amount The attempted stake amount. + * @param maxAmount The maximum allowed stake amount. + */ + error StakingAmountAboveMaximum(uint256 amount, uint256 maxAmount); + + /** + * @notice Thrown when there's no rewards to claim for the staked token. + * @param tokenAddress The address of the staked token. + * @param tokenId The ID of the staked token. + */ + error StakingNoRewardsToClaim(address tokenAddress, uint256 tokenId); + + /** + * @notice Thrown when attempting to unstake before the cooldown period has elapsed. + * @param stakedAt The timestamp when the tokens were staked. + * @param cooldownPeriod The required cooldown period in seconds. + * @param currentTime The current block timestamp. + */ + error StakingCooldownNotElapsed(uint256 stakedAt, uint256 cooldownPeriod, uint256 currentTime); + + /** + * @notice Thrown when owner is not the owner of the staked token. + * @param owner The address of the token owner. + * @param tokenAddress The address of the staked token. + * @param tokenId The ID of the staked token. + */ + error StakingNotTokenOwner(address owner, address tokenAddress, uint256 tokenId); + + /** + * @notice Thrown when an account has insufficient balance for a transfer or burn. + * @param _sender Address attempting the transfer. + * @param _balance Current balance of the sender. + * @param _needed Amount required to complete the operation. + */ + error StakingInsufficientBalance(address _sender, uint256 _balance, uint256 _needed); + + /** + * @notice Thrown when the sender address is invalid (e.g., zero address). + * @param _sender Invalid sender address. + */ + error StakingInvalidSender(address _sender); + + /** + * @notice Thrown when the receiver address is invalid (e.g., zero address). + * @param _receiver Invalid receiver address. + */ + error StakingInvalidReceiver(address _receiver); + + /** + * @notice Thrown when a spender tries to use more than the approved allowance. + * @param _spender Address attempting to spend. + * @param _allowance Current allowance for the spender. + * @param _needed Amount required to complete the operation. + */ + error StakingInsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + + /** + * @notice Emitted when tokens are successfully staked. + * @param staker The address of the user who staked the tokens. + * @param tokenAddress The address of the staked token. + * @param tokenId The ID of the staked token. + * @param amount The amount of tokens staked. + */ + event TokensStaked(address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 amount); + + /** + * @notice Emitted when tokens are successfully unstaked. + * @param staker The address of the user who unstaked the tokens. + * @param tokenAddress The address of the unstaked token. + * @param tokenId The ID of the unstaked token. + * @param amount The amount of tokens unstaked. + */ + event TokensUnstaked(address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 amount); + + /** + * @notice Emitted when rewards are claimed for staked tokens. + * @param staker The address of the user who claimed the rewards. + * @param tokenAddress The address of the staked token. + * @param tokenId The ID of the staked token. + * @param rewardAmount The amount of rewards claimed. + */ + event RewardsClaimed( + address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 rewardAmount + ); + + /** + * @notice Emitted when an approval is made for a spender by an owner. + * @param _owner The address granting the allowance. + * @param _spender The address receiving the allowance. + * @param _oldValue The previous allowance amount. + * @param _newValue The new allowance amount. + */ + event Approval(address indexed _owner, address indexed _spender, uint256 _oldValue, uint256 _newValue); + + /** + * @notice Emitted when tokens are transferred between two addresses. + * @param _from Address sending the tokens. + * @param _to Address receiving the tokens. + * @param _value Amount of tokens transferred. + */ + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + bytes32 constant STAKING_STORAGE_POSITION = keccak256("compose.staking"); + + /** + * @notice Structure containing staking information for a specific token. + * @param amount The amount of tokens staked. + * @param stakedAt The timestamp when the tokens were staked. + * @param lastClaimedAt The timestamp when rewards were last claimed. + * @param accumulatedRewards The total accumulated rewards for the staked tokens. + */ + struct StakedTokenInfo { + uint256 amount; + uint256 stakedAt; + uint256 lastClaimedAt; + uint256 accumulatedRewards; + } + + /** + * @notice Structure containing type of tokens being staked. + * @param isERC20 Boolean indicating if the token is ERC-20. + * @param isERC721 Boolean indicating if the token is ERC-721. + * @param isERC1155 Boolean indicating if the token is ERC-1155. + */ + struct TokenType { + bool isERC20; + bool isERC721; + bool isERC1155; + } + + /** + * @custom:storage-location erc8042:compose.staking + */ + struct StakingStorage { + mapping(address tokenType => TokenType) supportedTokens; + mapping(address tokenAddress => mapping(uint256 tokenId => StakedTokenInfo)) stakedTokens; + uint256 baseAPR; + uint256 rewardDecayRate; + uint256 compoundFrequency; + address rewardToken; + mapping(address tokenAddress => uint256 totalStaked) totalStakedPerToken; + uint256 cooldownPeriod; + uint256 maxStakeAmount; + uint256 minStakeAmount; + mapping(address user => mapping(address spender => uint256 allowance)) allowance; + } + + /** + * @notice Returns the staking storage structure from its predefined slot. + * @dev Uses inline assembly to access diamond storage location. + * @return s The storage reference to StakingStorage. + */ + function getStorage() internal pure returns (StakingStorage storage s) { + bytes32 position = STAKING_STORAGE_POSITION; + assembly { + s.slot := position + } + } + + /** + * @notice Stakes tokens of a supported type. + * @param _tokenAddress The address of the token to stake. + * @param _tokenId The ID of the token to stake (for ERC-721 and ERC-1155). + * @param _amount The amount of tokens to stake. + */ + function stakeToken(address _tokenAddress, uint256 _tokenId, uint256 _amount) external { + StakingStorage storage s = getStorage(); + + TokenType storage tokenType = s.supportedTokens[_tokenAddress]; + if (!tokenType.isERC20 && !tokenType.isERC721 && !tokenType.isERC1155) { + revert StakingUnsupportedToken(_tokenAddress); + } + if (_amount < s.minStakeAmount) { + revert StakingAmountBelowMinimum(_amount, s.minStakeAmount); + } + if (_amount > s.maxStakeAmount) { + revert StakingAmountAboveMaximum(_amount, s.maxStakeAmount); + } + + if (s.supportedTokens[_tokenAddress].isERC20) { + IERC20(_tokenAddress).transferFrom(msg.sender, address(this), _amount); + _stakeERC20(_tokenAddress, _amount); + } else if (s.supportedTokens[_tokenAddress].isERC721) { + IERC721(_tokenAddress).safeTransferFrom(msg.sender, address(this), _tokenId); + _stakeERC721(_tokenAddress, _tokenId); + } else if (s.supportedTokens[_tokenAddress].isERC1155) { + IERC1155(_tokenAddress).safeTransferFrom(msg.sender, address(this), _tokenId, _amount, ""); + _stakeERC1155(_tokenAddress, _tokenId, _amount); + } + } + + /** + * @notice Unstakes tokens of a supported type. + * @param _tokenAddress The address of the token to unstake. + * @param _tokenId The ID of the token to unstake (for ERC-721 and ERC-1155). + */ + function unstakeToken(address _tokenAddress, uint256 _tokenId) external { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + uint256 amount = stake.amount; + if (amount == 0) { + revert StakingZeroStakeAmount(); + } + + if (s.cooldownPeriod > 0 && block.timestamp <= stake.stakedAt + s.cooldownPeriod) { + revert StakingCooldownNotElapsed(stake.stakedAt, s.cooldownPeriod, block.timestamp); + } + + uint256 rewards = calculateRewards(_tokenAddress, _tokenId); + if (rewards > 0) { + _claimRewards(_tokenAddress, _tokenId); + } + + if (s.supportedTokens[_tokenAddress].isERC20) { + IERC20(_tokenAddress).transfer(msg.sender, amount); + } else if (s.supportedTokens[_tokenAddress].isERC721) { + IERC721(_tokenAddress).safeTransferFrom(address(this), msg.sender, _tokenId); + } else if (s.supportedTokens[_tokenAddress].isERC1155) { + IERC1155(_tokenAddress).safeTransferFrom(address(this), msg.sender, _tokenId, amount, ""); + } + + s.totalStakedPerToken[_tokenAddress] -= amount; + delete s.stakedTokens[_tokenAddress][_tokenId]; + + emit TokensUnstaked(msg.sender, _tokenAddress, _tokenId, amount); + } + + /** + * @notice An admin function to support a new token type for staking. + * @param _tokenAddress The address of the token to support. + * @param _isERC20 Boolean indicating if the token is ERC-20. + * @param _isERC721 Boolean indicating if the token is ERC-721. + * @param _isERC1155 Boolean indicating if the token is ERC-1155 + * @dev This function should be restricted to admin use only. + */ + function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) external { + StakingStorage storage s = getStorage(); + s.supportedTokens[_tokenAddress] = TokenType({isERC20: _isERC20, isERC721: _isERC721, isERC1155: _isERC1155}); + } + + /** + * @notice An admin function to set staking parameters. + * @param _baseAPR The base annual percentage rate for rewards. + * @param _rewardDecayRate The decay rate for rewards over time. + * @param _compoundFrequency The frequency at which rewards are compounded. + * @param _rewardToken The address of the token used for rewards. + * @param _cooldownPeriod The cooldown period before unstaking is allowed. + * @param _minStakeAmount The minimum amount required to stake. + * @param _maxStakeAmount The maximum amount allowed to stake. + * @dev This function should be restricted to admin use only. + */ + function setStakingParameters( + uint256 _baseAPR, + uint256 _rewardDecayRate, + uint256 _compoundFrequency, + address _rewardToken, + uint256 _cooldownPeriod, + uint256 _minStakeAmount, + uint256 _maxStakeAmount + ) external { + StakingStorage storage s = getStorage(); + s.baseAPR = _baseAPR; + s.rewardDecayRate = _rewardDecayRate; + s.compoundFrequency = _compoundFrequency; + s.rewardToken = _rewardToken; + s.cooldownPeriod = _cooldownPeriod; + s.minStakeAmount = _minStakeAmount; + s.maxStakeAmount = _maxStakeAmount; + } + + /** + * @notice Claims rewards for a staked token. + * @dev Updates the last claimed timestamp and accumulated rewards. + * @param _tokenAddress The address of the staked token. + * @param _tokenId The ID of the staked token. + */ + function claimRewards(address _tokenAddress, uint256 _tokenId) external { + _claimRewards(_tokenAddress, _tokenId); + } + + /** + * @notice Stake ERC-20 tokens + * @dev Transfers token from the user and updates the amount staked and staking info. + * @param _tokenAddress The address of the ERC-20 token to stake. + * @param _value The amount of tokens to stake. + */ + function _stakeERC20(address _tokenAddress, uint256 _value) internal { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][0]; + + stake.amount += _value; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; + + s.totalStakedPerToken[_tokenAddress] += _value; + } + + /** + * @notice Stake ERC-721 tokens + * @dev Transfers token from the user and updates the amount staked and staking info. + * @param _tokenAddress The address of the ERC-721 token to stake. + * @param _tokenId The ID of the token to stake. + */ + function _stakeERC721(address _tokenAddress, uint256 _tokenId) internal { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + stake.amount = 1; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; + + s.totalStakedPerToken[_tokenAddress] += 1; + } + + /** + * @notice Stake ERC-1155 tokens + * @dev Transfers token from the user and updates the amount staked and staking info. + * @param _tokenAddress The address of the ERC-1155 token to stake. + * @param _tokenId The ID of the token to stake. + * @param _value The amount of tokens to stake. + */ + function _stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) internal { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + stake.amount += _value; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; + + s.totalStakedPerToken[_tokenAddress] += _value; + } + + /** + * @notice Calculates the rewards for a staked token. + * @dev Uses base APR, decay rate, and compounding frequency to compute rewards. + * @param _tokenAddress The address of the staked token. + * @param _tokenId The ID of the staked token. + * @return finalReward The calculated reward amount. + */ + function calculateRewards(address _tokenAddress, uint256 _tokenId) internal view returns (uint256) { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + uint256 stakedDuration = block.timestamp - stake.lastClaimedAt; + if (stakedDuration == 0 || stake.amount == 0) { + return 0; + } + + uint256 baseReward = (stake.amount * s.baseAPR * stakedDuration) / (365 days * 100); + + uint256 decayFactor = + s.rewardDecayRate > 0 ? (s.rewardDecayRate ** (stakedDuration / s.compoundFrequency)) : 10 ** 18; + + uint256 finalReward = (baseReward * decayFactor) / (10 ** 18); + + return finalReward; + } + + /** + * @notice Claimes rewards for a staked token. + * @dev Internal function to update staking info after rewards are claimed. + * @param _tokenAddress The address of the staked token. + * @param _tokenId The ID of the staked token. + */ + function _claimRewards(address _tokenAddress, uint256 _tokenId) internal { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + uint256 rewards = calculateRewards(_tokenAddress, _tokenId); + if (rewards == 0) { + return; + } + + IERC20(s.rewardToken).transfer(msg.sender, rewards); + + stake.lastClaimedAt = block.timestamp; + stake.accumulatedRewards += rewards; + + emit RewardsClaimed(msg.sender, _tokenAddress, _tokenId, rewards); + } + + /** + * @notice Support Interface to satisfy ERC-165 standard. + * @param interfaceId The interface identifier, as specified in ERC-165. + * @return True if the contract implements the requested interface. + */ + function supportsInterface(bytes4 interfaceId) external pure returns (bool) { + return interfaceId == type(IERC721Receiver).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId; } } diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol index 9e498aa6..93b21960 100644 --- a/src/token/Staking/StakingMod.sol +++ b/src/token/Staking/StakingMod.sol @@ -2,520 +2,109 @@ pragma solidity >=0.8.30; /** - * @dev Simplified ERC20 interface. + * @title Staking Library for Compose + * @notice Provides internal logic for staking functionality using diamond storage. + * This library is intended to be used by custom facets to integrate with staking features. + * @dev Uses ERC-8042 for storage location standardization. */ -interface IERC20 { - function totalSupply() external view returns (uint256); - function balanceOf(address account) external view returns (uint256); +bytes32 constant STAKING_STORAGE_POSITION = keccak256("compose.staking"); - function transfer(address to, uint256 amount) external returns (bool); - - function allowance(address owner, address spender) external view returns (uint256); - - function approve(address spender, uint256 amount) external returns (bool); - - function transferFrom(address from, address to, uint256 amount) external returns (bool); +/** + * @notice Structure containing staking information for a specific token. + * @param amount The amount of tokens staked. + * @param stakedAt The timestamp when the tokens were staked. + * @param lastClaimedAt The timestamp when rewards were last claimed. + * @param accumulatedRewards The total accumulated rewards for the staked tokens. + */ +struct StakedTokenInfo { + uint256 amount; + uint256 stakedAt; + uint256 lastClaimedAt; + uint256 accumulatedRewards; } /** - * @dev Simplified ERC721 interface. + * @notice Structure containing type of tokens being staked. + * @param isERC20 Boolean indicating if the token is ERC-20. + * @param isERC721 Boolean indicating if the token is ERC-721. + * @param isERC1155 Boolean indicating if the token is ERC-1155. */ -interface IERC721 { - function ownerOf(uint256 tokenId) external view returns (address owner); - - function safeTransferFrom(address from, address to, uint256 tokenId) external; +struct TokenType { + bool isERC20; + bool isERC721; + bool isERC1155; } /** - * @dev Simplified ERC1155 interface. + * @custom:storage-location erc8042:compose.staking */ -interface IERC1155 { - function balanceOf(address account, uint256 id) external view returns (uint256); - function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external; +struct StakingStorage { + mapping(address tokenType => TokenType) supportedTokens; + mapping(address tokenAddress => mapping(uint256 tokenId => StakedTokenInfo)) stakedTokens; + uint256 baseAPR; + uint256 rewardDecayRate; + uint256 compoundFrequency; + address rewardToken; + mapping(address tokenAddress => uint256 totalStaked) totalStakedPerToken; + uint256 cooldownPeriod; + uint256 maxStakeAmount; + uint256 minStakeAmount; + mapping(address user => mapping(address spender => uint256 allowance)) allowance; } /** - * @title ERC-721 Token Receiver Interface - * @notice Interface for contracts that want to handle safe transfers of ERC-721 tokens. - * @dev Contracts implementing this must return the selector to confirm token receipt. + * @notice Returns the staking storage structure from its predefined slot. + * @dev Uses inline assembly to access diamond storage location. + * @return s The storage reference to StakingStorage. */ -interface IERC721Receiver { - /** - * @notice Handles the receipt of an NFT. - * @param _operator The address which called `safeTransferFrom`. - * @param _from The previous owner of the token. - * @param _tokenId The NFT identifier being transferred. - * @param _data Additional data with no specified format. - * @return The selector to confirm the token transfer. - */ - function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes calldata _data) - external - returns (bytes4); +function getStorage() internal pure returns (StakingStorage storage s) { + bytes32 position = STAKING_STORAGE_POSITION; + assembly { + s.slot := position + } } /** - * @title ERC-1155 Token Receiver Interface - * @notice Interface that must be implemented by smart contracts in order to receive ERC-1155 token transfers. + * @notice An admin function to support a new token type for staking. + * @param _tokenAddress The address of the token to support. + * @param _isERC20 Boolean indicating if the token is ERC-20. + * @param _isERC721 Boolean indicating if the token is ERC-721. + * @param _isERC1155 Boolean indicating if the token is ERC-1155 + * @dev This function should be restricted to admin use only. */ -interface IERC1155Receiver { - /** - * @notice Handles the receipt of a single ERC-1155 token type. - * @dev This function is called at the end of a `safeTransferFrom` after the balance has been updated. - * - * IMPORTANT: To accept the transfer, this must return - * `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` - * (i.e. 0xf23a6e61, or its own function selector). - * - * @param _operator The address which initiated the transfer (i.e. msg.sender). - * @param _from The address which previously owned the token. - * @param _id The ID of the token being transferred. - * @param _value The amount of tokens being transferred. - * @param _data Additional data with no specified format. - * @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed. - */ - function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data) - external - returns (bytes4); - - /** - * @notice Handles the receipt of multiple ERC-1155 token types. - * @dev This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. - * - * IMPORTANT: To accept the transfer(s), this must return - * `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` - * (i.e. 0xbc197c81, or its own function selector). - * - * @param _operator The address which initiated the batch transfer (i.e. msg.sender). - * @param _from The address which previously owned the token. - * @param _ids An array containing ids of each token being transferred (order and length must match _values array). - * @param _values An array containing amounts of each token being transferred (order and length must match _ids array). - * @param _data Additional data with no specified format. - * @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed. - */ - function onERC1155BatchReceived( - address _operator, - address _from, - uint256[] calldata _ids, - uint256[] calldata _values, - bytes calldata _data - ) external returns (bytes4); +function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) { + StakingStorage storage s = getStorage(); + s.supportedTokens[_tokenAddress] = TokenType({isERC20: _isERC20, isERC721: _isERC721, isERC1155: _isERC1155}); } -contract StakingFacet { - /** - * @title LibStaking - Standard Staking Library - * @notice Provides internal functions and storage layout for staking ERC-20, ERC-721, and ERC-1155 tokens. - * @dev Uses ERC-8042 for storage location standardization. - */ - /** - * @notice Thrown when attempting to stake an unsupported token type. - * @param tokenAddress The address of the unsupported token. - */ - error StakingUnsupportedToken(address tokenAddress); - - /** - * @notice Thrown when attempting to stake a zero amount. - */ - error StakingZeroStakeAmount(); - - /** - * @notice Thrown when attempting to stake an amount below the minimum stake amount. - * @param amount The attempted stake amount. - * @param minAmount The minimum required stake amount. - */ - error StakingAmountBelowMinimum(uint256 amount, uint256 minAmount); - - /** - * @notice Thrown when attempting to stake an amount above the maximum stake amount. - * @param amount The attempted stake amount. - * @param maxAmount The maximum allowed stake amount. - */ - error StakingAmountAboveMaximum(uint256 amount, uint256 maxAmount); - - /** - * @notice Thrown when there's no rewards to claim for the staked token. - * @param tokenAddress The address of the staked token. - * @param tokenId The ID of the staked token. - */ - error StakingNoRewardsToClaim(address tokenAddress, uint256 tokenId); - - /** - * @notice Thrown when attempting to unstake before the cooldown period has elapsed. - * @param stakedAt The timestamp when the tokens were staked. - * @param cooldownPeriod The required cooldown period in seconds. - * @param currentTime The current block timestamp. - */ - error StakingCooldownNotElapsed(uint256 stakedAt, uint256 cooldownPeriod, uint256 currentTime); - - /** - * @notice Thrown when owner is not the owner of the staked token. - * @param owner The address of the token owner. - * @param tokenAddress The address of the staked token. - * @param tokenId The ID of the staked token. - */ - error StakingNotTokenOwner(address owner, address tokenAddress, uint256 tokenId); - - /** - * @notice Thrown when an account has insufficient balance for a transfer or burn. - * @param _sender Address attempting the transfer. - * @param _balance Current balance of the sender. - * @param _needed Amount required to complete the operation. - */ - error StakingInsufficientBalance(address _sender, uint256 _balance, uint256 _needed); - - /** - * @notice Thrown when the sender address is invalid (e.g., zero address). - * @param _sender Invalid sender address. - */ - error StakingInvalidSender(address _sender); - - /** - * @notice Thrown when the receiver address is invalid (e.g., zero address). - * @param _receiver Invalid receiver address. - */ - error StakingInvalidReceiver(address _receiver); - - /** - * @notice Thrown when a spender tries to use more than the approved allowance. - * @param _spender Address attempting to spend. - * @param _allowance Current allowance for the spender. - * @param _needed Amount required to complete the operation. - */ - error StakingInsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); - - /** - * @notice Emitted when tokens are successfully staked. - * @param staker The address of the user who staked the tokens. - * @param tokenAddress The address of the staked token. - * @param tokenId The ID of the staked token. - * @param amount The amount of tokens staked. - */ - event TokensStaked(address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 amount); - - /** - * @notice Emitted when tokens are successfully unstaked. - * @param staker The address of the user who unstaked the tokens. - * @param tokenAddress The address of the unstaked token. - * @param tokenId The ID of the unstaked token. - * @param amount The amount of tokens unstaked. - */ - event TokensUnstaked(address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 amount); - - /** - * @notice Emitted when rewards are claimed for staked tokens. - * @param staker The address of the user who claimed the rewards. - * @param tokenAddress The address of the staked token. - * @param tokenId The ID of the staked token. - * @param rewardAmount The amount of rewards claimed. - */ - event RewardsClaimed( - address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 rewardAmount - ); - - /** - * @notice Emitted when an approval is made for a spender by an owner. - * @param _owner The address granting the allowance. - * @param _spender The address receiving the allowance. - * @param _oldValue The previous allowance amount. - * @param _newValue The new allowance amount. - */ - event Approval(address indexed _owner, address indexed _spender, uint256 _oldValue, uint256 _newValue); - - /** - * @notice Emitted when tokens are transferred between two addresses. - * @param _from Address sending the tokens. - * @param _to Address receiving the tokens. - * @param _value Amount of tokens transferred. - */ - event Transfer(address indexed _from, address indexed _to, uint256 _value); - - bytes32 constant STAKING_STORAGE_POSITION = keccak256("compose.staking"); - - /** - * @notice Structure containing staking information for a specific token. - * @param amount The amount of tokens staked. - * @param stakedAt The timestamp when the tokens were staked. - * @param lastClaimedAt The timestamp when rewards were last claimed. - * @param accumulatedRewards The total accumulated rewards for the staked tokens. - */ - struct StakedTokenInfo { - uint256 amount; - uint256 stakedAt; - uint256 lastClaimedAt; - uint256 accumulatedRewards; - } - - /** - * @notice Structure containing type of tokens being staked. - * @param isERC20 Boolean indicating if the token is ERC-20. - * @param isERC721 Boolean indicating if the token is ERC-721. - * @param isERC1155 Boolean indicating if the token is ERC-1155. - */ - struct TokenType { - bool isERC20; - bool isERC721; - bool isERC1155; - } - - /** - * @custom:storage-location erc8042:compose.staking - */ - struct StakingStorage { - mapping(address tokenType => TokenType) supportedTokens; - mapping(address tokenAddress => mapping(uint256 tokenId => StakedTokenInfo)) stakedTokens; - uint256 baseAPR; - uint256 rewardDecayRate; - uint256 compoundFrequency; - address rewardToken; - mapping(address tokenAddress => uint256 totalStaked) totalStakedPerToken; - uint256 cooldownPeriod; - uint256 maxStakeAmount; - uint256 minStakeAmount; - mapping(address user => mapping(address spender => uint256 allowance)) allowance; - } - - /** - * @notice Returns the staking storage structure from its predefined slot. - * @dev Uses inline assembly to access diamond storage location. - * @return s The storage reference to StakingStorage. - */ - function getStorage() internal pure returns (StakingStorage storage s) { - bytes32 position = STAKING_STORAGE_POSITION; - assembly { - s.slot := position - } - } - - /** - * @notice Stakes tokens of a supported type. - * @param _tokenAddress The address of the token to stake. - * @param _tokenId The ID of the token to stake (for ERC-721 and ERC-1155). - * @param _amount The amount of tokens to stake. - */ - function stakeToken(address _tokenAddress, uint256 _tokenId, uint256 _amount) external { - StakingStorage storage s = getStorage(); - - TokenType storage tokenType = s.supportedTokens[_tokenAddress]; - if (!tokenType.isERC20 && !tokenType.isERC721 && !tokenType.isERC1155) { - revert StakingUnsupportedToken(_tokenAddress); - } - if (_amount < s.minStakeAmount) { - revert StakingAmountBelowMinimum(_amount, s.minStakeAmount); - } - if (_amount > s.maxStakeAmount) { - revert StakingAmountAboveMaximum(_amount, s.maxStakeAmount); - } - - if (s.supportedTokens[_tokenAddress].isERC20) { - IERC20(_tokenAddress).transferFrom(msg.sender, address(this), _amount); - _stakeERC20(_tokenAddress, _amount); - } else if (s.supportedTokens[_tokenAddress].isERC721) { - IERC721(_tokenAddress).safeTransferFrom(msg.sender, address(this), _tokenId); - _stakeERC721(_tokenAddress, _tokenId); - } else if (s.supportedTokens[_tokenAddress].isERC1155) { - IERC1155(_tokenAddress).safeTransferFrom(msg.sender, address(this), _tokenId, _amount, ""); - _stakeERC1155(_tokenAddress, _tokenId, _amount); - } - } - - /** - * @notice Unstakes tokens of a supported type. - * @param _tokenAddress The address of the token to unstake. - * @param _tokenId The ID of the token to unstake (for ERC-721 and ERC-1155). - */ - function unstakeToken(address _tokenAddress, uint256 _tokenId) external { - StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; - - uint256 amount = stake.amount; - if (amount == 0) { - revert StakingZeroStakeAmount(); - } - - if (s.cooldownPeriod > 0 && block.timestamp <= stake.stakedAt + s.cooldownPeriod) { - revert StakingCooldownNotElapsed(stake.stakedAt, s.cooldownPeriod, block.timestamp); - } - - uint256 rewards = calculateRewards(_tokenAddress, _tokenId); - if (rewards > 0) { - _claimRewards(_tokenAddress, _tokenId); - } - - if (s.supportedTokens[_tokenAddress].isERC20) { - IERC20(_tokenAddress).transfer(msg.sender, amount); - } else if (s.supportedTokens[_tokenAddress].isERC721) { - IERC721(_tokenAddress).safeTransferFrom(address(this), msg.sender, _tokenId); - } else if (s.supportedTokens[_tokenAddress].isERC1155) { - IERC1155(_tokenAddress).safeTransferFrom(address(this), msg.sender, _tokenId, amount, ""); - } - - s.totalStakedPerToken[_tokenAddress] -= amount; - delete s.stakedTokens[_tokenAddress][_tokenId]; - - emit TokensUnstaked(msg.sender, _tokenAddress, _tokenId, amount); - } - - /** - * @notice An admin function to support a new token type for staking. - * @param _tokenAddress The address of the token to support. - * @param _isERC20 Boolean indicating if the token is ERC-20. - * @param _isERC721 Boolean indicating if the token is ERC-721. - * @param _isERC1155 Boolean indicating if the token is ERC-1155 - * @dev This function should be restricted to admin use only. - */ - function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) external { - StakingStorage storage s = getStorage(); - s.supportedTokens[_tokenAddress] = TokenType({isERC20: _isERC20, isERC721: _isERC721, isERC1155: _isERC1155}); - } - - /** - * @notice An admin function to set staking parameters. - * @param _baseAPR The base annual percentage rate for rewards. - * @param _rewardDecayRate The decay rate for rewards over time. - * @param _compoundFrequency The frequency at which rewards are compounded. - * @param _rewardToken The address of the token used for rewards. - * @param _cooldownPeriod The cooldown period before unstaking is allowed. - * @param _minStakeAmount The minimum amount required to stake. - * @param _maxStakeAmount The maximum amount allowed to stake. - * @dev This function should be restricted to admin use only. - */ - function setStakingParameters( - uint256 _baseAPR, - uint256 _rewardDecayRate, - uint256 _compoundFrequency, - address _rewardToken, - uint256 _cooldownPeriod, - uint256 _minStakeAmount, - uint256 _maxStakeAmount - ) external { - StakingStorage storage s = getStorage(); - s.baseAPR = _baseAPR; - s.rewardDecayRate = _rewardDecayRate; - s.compoundFrequency = _compoundFrequency; - s.rewardToken = _rewardToken; - s.cooldownPeriod = _cooldownPeriod; - s.minStakeAmount = _minStakeAmount; - s.maxStakeAmount = _maxStakeAmount; - } - - /** - * @notice Claims rewards for a staked token. - * @dev Updates the last claimed timestamp and accumulated rewards. - * @param _tokenAddress The address of the staked token. - * @param _tokenId The ID of the staked token. - */ - function claimRewards(address _tokenAddress, uint256 _tokenId) external { - _claimRewards(_tokenAddress, _tokenId); - } - - /** - * @notice Stake ERC-20 tokens - * @dev Transfers token from the user and updates the amount staked and staking info. - * @param _tokenAddress The address of the ERC-20 token to stake. - * @param _value The amount of tokens to stake. - */ - function _stakeERC20(address _tokenAddress, uint256 _value) internal { - StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][0]; - - stake.amount += _value; - stake.stakedAt = block.timestamp; - stake.lastClaimedAt = block.timestamp; - - s.totalStakedPerToken[_tokenAddress] += _value; - } - - /** - * @notice Stake ERC-721 tokens - * @dev Transfers token from the user and updates the amount staked and staking info. - * @param _tokenAddress The address of the ERC-721 token to stake. - * @param _tokenId The ID of the token to stake. - */ - function _stakeERC721(address _tokenAddress, uint256 _tokenId) internal { - StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; - - stake.amount = 1; - stake.stakedAt = block.timestamp; - stake.lastClaimedAt = block.timestamp; - - s.totalStakedPerToken[_tokenAddress] += 1; - } - - /** - * @notice Stake ERC-1155 tokens - * @dev Transfers token from the user and updates the amount staked and staking info. - * @param _tokenAddress The address of the ERC-1155 token to stake. - * @param _tokenId The ID of the token to stake. - * @param _value The amount of tokens to stake. - */ - function _stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) internal { - StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; - - stake.amount += _value; - stake.stakedAt = block.timestamp; - stake.lastClaimedAt = block.timestamp; - - s.totalStakedPerToken[_tokenAddress] += _value; - } - - /** - * @notice Calculates the rewards for a staked token. - * @dev Uses base APR, decay rate, and compounding frequency to compute rewards. - * @param _tokenAddress The address of the staked token. - * @param _tokenId The ID of the staked token. - * @return finalReward The calculated reward amount. - */ - function calculateRewards(address _tokenAddress, uint256 _tokenId) internal view returns (uint256) { - StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; - - uint256 stakedDuration = block.timestamp - stake.lastClaimedAt; - if (stakedDuration == 0 || stake.amount == 0) { - return 0; - } - - uint256 baseReward = (stake.amount * s.baseAPR * stakedDuration) / (365 days * 100); - - uint256 decayFactor = - s.rewardDecayRate > 0 ? (s.rewardDecayRate ** (stakedDuration / s.compoundFrequency)) : 10 ** 18; - - uint256 finalReward = (baseReward * decayFactor) / (10 ** 18); - - return finalReward; - } - - /** - * @notice Claimes rewards for a staked token. - * @dev Internal function to update staking info after rewards are claimed. - * @param _tokenAddress The address of the staked token. - * @param _tokenId The ID of the staked token. - */ - function _claimRewards(address _tokenAddress, uint256 _tokenId) internal { - StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; - - uint256 rewards = calculateRewards(_tokenAddress, _tokenId); - if (rewards == 0) { - return; - } - - IERC20(s.rewardToken).transfer(msg.sender, rewards); - - stake.lastClaimedAt = block.timestamp; - stake.accumulatedRewards += rewards; - - emit RewardsClaimed(msg.sender, _tokenAddress, _tokenId, rewards); - } - - /** - * @notice Support Interface to satisfy ERC-165 standard. - * @param interfaceId The interface identifier, as specified in ERC-165. - * @return True if the contract implements the requested interface. - */ - function supportsInterface(bytes4 interfaceId) external pure returns (bool) { - return interfaceId == type(IERC721Receiver).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId; - } +/** + * @notice An admin function to set staking parameters. + * @param _baseAPR The base annual percentage rate for rewards. + * @param _rewardDecayRate The decay rate for rewards over time. + * @param _compoundFrequency The frequency at which rewards are compounded. + * @param _rewardToken The address of the token used for rewards. + * @param _cooldownPeriod The cooldown period before unstaking is allowed. + * @param _minStakeAmount The minimum amount required to stake. + * @param _maxStakeAmount The maximum amount allowed to stake. + * @dev This function should be restricted to admin use only. + */ +function setStakingParameters( + uint256 _baseAPR, + uint256 _rewardDecayRate, + uint256 _compoundFrequency, + address _rewardToken, + uint256 _cooldownPeriod, + uint256 _minStakeAmount, + uint256 _maxStakeAmount +) external { + StakingStorage storage s = getStorage(); + s.baseAPR = _baseAPR; + s.rewardDecayRate = _rewardDecayRate; + s.compoundFrequency = _compoundFrequency; + s.rewardToken = _rewardToken; + s.cooldownPeriod = _cooldownPeriod; + s.minStakeAmount = _minStakeAmount; + s.maxStakeAmount = _maxStakeAmount; } + From 8d31a0ddb76c263c7492e6c5394ca259ac5e524f Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Wed, 17 Dec 2025 16:00:31 +0800 Subject: [PATCH 03/20] Staking Facet and revise Staking Mod --- src/token/Staking/StakingMod.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol index 93b21960..b59d8c59 100644 --- a/src/token/Staking/StakingMod.sol +++ b/src/token/Staking/StakingMod.sol @@ -8,6 +8,9 @@ pragma solidity >=0.8.30; * @dev Uses ERC-8042 for storage location standardization. */ +/** + * @dev Storage position constant defined via keccak256 hash of diamond storage identifier. + */ bytes32 constant STAKING_STORAGE_POSITION = keccak256("compose.staking"); /** @@ -58,7 +61,7 @@ struct StakingStorage { * @dev Uses inline assembly to access diamond storage location. * @return s The storage reference to StakingStorage. */ -function getStorage() internal pure returns (StakingStorage storage s) { +function getStorage() pure returns (StakingStorage storage s) { bytes32 position = STAKING_STORAGE_POSITION; assembly { s.slot := position @@ -97,7 +100,7 @@ function setStakingParameters( uint256 _cooldownPeriod, uint256 _minStakeAmount, uint256 _maxStakeAmount -) external { +) { StakingStorage storage s = getStorage(); s.baseAPR = _baseAPR; s.rewardDecayRate = _rewardDecayRate; From 5b0e947d0d8d46cff972933e5c35eb66d3a7f6b3 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Thu, 18 Dec 2025 12:49:47 +0800 Subject: [PATCH 04/20] added internal functions in StakingMod --- src/token/Staking/StakingMod.sol | 52 +++++++++++++++++++ .../Staking/harnesses/StakingFacetHarness.sol | 11 ++++ 2 files changed, 63 insertions(+) create mode 100644 test/token/Staking/harnesses/StakingFacetHarness.sol diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol index b59d8c59..3c11f644 100644 --- a/src/token/Staking/StakingMod.sol +++ b/src/token/Staking/StakingMod.sol @@ -111,3 +111,55 @@ function setStakingParameters( s.maxStakeAmount = _maxStakeAmount; } +/** + * @notice Stake ERC-20 tokens + * @dev Transfers token from the user and updates the amount staked and staking info. + * @param _tokenAddress The address of the ERC-20 token to stake. + * @param _value The amount of tokens to stake. + */ +function _stakeERC20(address _tokenAddress, uint256 _value) { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][0]; + + stake.amount += _value; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; + + s.totalStakedPerToken[_tokenAddress] += _value; +} + +/** + * @notice Stake ERC-721 tokens + * @dev Transfers token from the user and updates the amount staked and staking info. + * @param _tokenAddress The address of the ERC-721 token to stake. + * @param _tokenId The ID of the token to stake. + */ +function _stakeERC721(address _tokenAddress, uint256 _tokenId) { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + stake.amount = 1; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; + + s.totalStakedPerToken[_tokenAddress] += 1; +} + +/** + * @notice Stake ERC-1155 tokens + * @dev Transfers token from the user and updates the amount staked and staking info. + * @param _tokenAddress The address of the ERC-1155 token to stake. + * @param _tokenId The ID of the token to stake. + * @param _value The amount of tokens to stake. + */ +function _stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + + stake.amount += _value; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; + + s.totalStakedPerToken[_tokenAddress] += _value; +} + diff --git a/test/token/Staking/harnesses/StakingFacetHarness.sol b/test/token/Staking/harnesses/StakingFacetHarness.sol new file mode 100644 index 00000000..7bac8739 --- /dev/null +++ b/test/token/Staking/harnesses/StakingFacetHarness.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {StakingFacet} from "../../../../src/token/Staking/StakingFacet.sol"; + +/** + * @title StakingFacetHarness + * @notice Test harness for StakingFacet + * @dev Adds helper functions to set up staking state for testing + */ +contract StakingFacetHarness is StakingFacet {} From 867da5d36248b9ab760846d95b3c382279fb8857 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Thu, 18 Dec 2025 16:38:30 +0800 Subject: [PATCH 05/20] Staking library test --- src/token/Staking/StakingMod.sol | 57 +++++++++- test/token/Staking/Staking.t.sol | 107 ++++++++++++++++++ .../Staking/harnesses/LibStakingHarness.sol | 67 +++++++++++ 3 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 test/token/Staking/Staking.t.sol create mode 100644 test/token/Staking/harnesses/LibStakingHarness.sol diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol index 3c11f644..6f2c956a 100644 --- a/src/token/Staking/StakingMod.sol +++ b/src/token/Staking/StakingMod.sol @@ -117,7 +117,7 @@ function setStakingParameters( * @param _tokenAddress The address of the ERC-20 token to stake. * @param _value The amount of tokens to stake. */ -function _stakeERC20(address _tokenAddress, uint256 _value) { +function stakeERC20(address _tokenAddress, uint256 _value) { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][0]; @@ -134,7 +134,7 @@ function _stakeERC20(address _tokenAddress, uint256 _value) { * @param _tokenAddress The address of the ERC-721 token to stake. * @param _tokenId The ID of the token to stake. */ -function _stakeERC721(address _tokenAddress, uint256 _tokenId) { +function stakeERC721(address _tokenAddress, uint256 _tokenId) { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; @@ -152,7 +152,7 @@ function _stakeERC721(address _tokenAddress, uint256 _tokenId) { * @param _tokenId The ID of the token to stake. * @param _value The amount of tokens to stake. */ -function _stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) { +function stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; @@ -163,3 +163,54 @@ function _stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) s.totalStakedPerToken[_tokenAddress] += _value; } +/** + * @notice Retrieve staking parameters + * @return baseAPR The base annual percentage rate for rewards. + * @return rewardDecayRate The decay rate for rewards over time. + * @return compoundFrequency The frequency at which rewards are compounded. + * @return rewardToken The address of the token used for rewards. + * @return cooldownPeriod The cooldown period before unstaking is allowed. + * @return minStakeAmount The minimum amount required to stake. + * @return maxStakeAmount The maximum amount allowed to stake. + */ +function getStakingParameters() + view + returns ( + uint256 baseAPR, + uint256 rewardDecayRate, + uint256 compoundFrequency, + address rewardToken, + uint256 cooldownPeriod, + uint256 minStakeAmount, + uint256 maxStakeAmount + ) +{ + StakingStorage storage s = getStorage(); + return ( + s.baseAPR, + s.rewardDecayRate, + s.compoundFrequency, + s.rewardToken, + s.cooldownPeriod, + s.minStakeAmount, + s.maxStakeAmount + ); +} + +/** + * @notice Retrieve staked token info for ERC-20, ERC-721, or ERC-1155 tokens + * @param _tokenAddress The address of the token. + * @param _tokenId The ID of the token (0 for ERC-20). + * @return amount The amount of tokens staked. + * @return stakedAt The timestamp when the tokens were staked. + * @return lastClaimedAt The timestamp when rewards were last claimed. + * @return accumulatedRewards The total accumulated rewards for the staked tokens. + */ +function getStakedTokenInfo(address _tokenAddress, uint256 _tokenId) + view + returns (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) +{ + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + return (stake.amount, stake.stakedAt, stake.lastClaimedAt, stake.accumulatedRewards); +} diff --git a/test/token/Staking/Staking.t.sol b/test/token/Staking/Staking.t.sol new file mode 100644 index 00000000..e129223a --- /dev/null +++ b/test/token/Staking/Staking.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {LibStakingHarness} from "./harnesses/LibStakingHarness.sol"; +import {ERC20FacetHarness} from "../ERC20//ERC20/harnesses/ERC20FacetHarness.sol"; +import {ERC721FacetHarness} from "../ERC721/ERC721/harnesses/ERC721FacetHarness.sol"; +import {ERC1155FacetHarness} from "../ERC1155/ERC1155/harnesses/ERC1155FacetHarness.sol"; + +// import "forge-std/console.sol"; + +contract StakingTest is Test { + ERC20FacetHarness erc20Token; + ERC20FacetHarness rewardToken; + ERC721FacetHarness erc721Token; + ERC1155FacetHarness erc1155Token; + LibStakingHarness staking; + + address public alice; + address public bob; + + string constant ASSET_NAME = "Test Token"; + string constant ASSET_SYMBOL = "TEST"; + uint8 constant ASSET_DECIMALS = 18; + + string constant TOKEN_NAME = "Test Token"; + string constant TOKEN_SYMBOL = "TEST"; + string constant BASE_URI = "https://example.com/api/nft/"; + + string constant DEFAULT_URI = "https://token.uri/{id}.json"; + string constant BASE_URI_1155 = "https://base.uri/"; + string constant TOKEN_URI = "token1.json"; + + uint256 constant TOKEN_ID_1 = 1; + uint256 constant TOKEN_ID_2 = 2; + uint256 constant TOKEN_ID_3 = 3; + + uint256 constant BASE_APR = 500; // 5% + uint256 constant REWARD_DECAY_RATE = 50; // 0.5% + uint256 constant COMPOUND_FREQUENCY = 1 days; + uint256 constant COOLDOWN_PERIOD = 7 days; + uint256 constant MIN_STAKE_AMOUNT = 1 ether; + uint256 constant MAX_STAKE_AMOUNT = 1000 ether; + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + + /** + * Deploy and initialize the reward ERC-20 token + */ + rewardToken = new ERC20FacetHarness(); + rewardToken.initialize("Reward Token", "RWD", 18); + + /** + * Deploy and initialize the ERC-20 token for staking tests + */ + erc20Token = new ERC20FacetHarness(); + erc20Token.initialize(ASSET_NAME, ASSET_SYMBOL, ASSET_DECIMALS); + + /** + * Deploy and initialize the ERC-721 token for staking tests + */ + erc721Token = new ERC721FacetHarness(); + erc721Token.initialize(TOKEN_NAME, TOKEN_SYMBOL, BASE_URI); + + /** + * Deploy and initialize the ERC-1155 token for staking tests + */ + erc1155Token = new ERC1155FacetHarness(); + erc1155Token.initialize(DEFAULT_URI); + + /** + * Deploy and initialize the staking harness + */ + staking = new LibStakingHarness(); + staking.initialize( + BASE_APR, + REWARD_DECAY_RATE, + COMPOUND_FREQUENCY, + address(rewardToken), + COOLDOWN_PERIOD, + MIN_STAKE_AMOUNT, + MAX_STAKE_AMOUNT + ); + } + + function test_ParametersAreSetCorrectly() public { + ( + uint256 baseAPR, + uint256 rewardDecayRate, + uint256 compoundFrequency, + address rewardTokenAddress, + uint256 cooldownPeriod, + uint256 minStakeAmount, + uint256 maxStakeAmount + ) = staking.getStakingParameters(); + + assertEq(baseAPR, BASE_APR); + assertEq(rewardDecayRate, REWARD_DECAY_RATE); + assertEq(compoundFrequency, COMPOUND_FREQUENCY); + assertEq(rewardTokenAddress, address(rewardToken)); + assertEq(cooldownPeriod, COOLDOWN_PERIOD); + assertEq(minStakeAmount, MIN_STAKE_AMOUNT); + assertEq(maxStakeAmount, MAX_STAKE_AMOUNT); + } +} diff --git a/test/token/Staking/harnesses/LibStakingHarness.sol b/test/token/Staking/harnesses/LibStakingHarness.sol new file mode 100644 index 00000000..9690e340 --- /dev/null +++ b/test/token/Staking/harnesses/LibStakingHarness.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import "../../../../src/token/Staking/StakingMod.sol" as StakingMod; + +/** + * @title LibStakingHarness + * @notice Test harness that exposes internal functions of LibStaking as external + * @dev Required for testing since StakingMod functions are internal + */ +contract LibStakingHarness { + /** + * @notice Initialize the staking storage for testing + * @dev Only used for testing purposes + */ + function initialize( + uint256 _baseAPR, + uint256 _rewardDecayRate, + uint256 _compoundFrequency, + address _rewardToken, + uint256 _cooldownPeriod, + uint256 _minStakeAmount, + uint256 _maxStakeAmount + ) external { + StakingMod.setStakingParameters( + _baseAPR, + _rewardDecayRate, + _compoundFrequency, + _rewardToken, + _cooldownPeriod, + _minStakeAmount, + _maxStakeAmount + ); + } + + /** + * @notice Exposes StakingMod._stakeERC20 as an external function + */ + function stakeERC20(address _tokenAddress, uint256 _value) external { + StakingMod.stakeERC20(_tokenAddress, _value); + } + + /** + * @notice Exposes StakingMod._stakeERC721 as an external function + */ + function stakeERC721(address _tokenAddress, uint256 _tokenId) external { + StakingMod.stakeERC721(_tokenAddress, _tokenId); + } + + /** + * @notice Exposes StakingMod._stakeERC1155 as an external function + */ + function stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) external { + StakingMod.stakeERC1155(_tokenAddress, _tokenId, _value); + } + + /** + * @notice Exposes StakingMod.getStakingParameters as an external function + */ + function getStakingParameters() + external + view + returns (uint256, uint256, uint256, address, uint256, uint256, uint256) + { + return StakingMod.getStakingParameters(); + } +} From c4aaeb782fe1714e2527330d076740bfa2683b61 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Thu, 18 Dec 2025 16:51:30 +0800 Subject: [PATCH 06/20] Staking library test --- test/token/Staking/Staking.t.sol | 39 +++++++++++++++++++ .../Staking/harnesses/LibStakingHarness.sol | 11 ++++++ 2 files changed, 50 insertions(+) diff --git a/test/token/Staking/Staking.t.sol b/test/token/Staking/Staking.t.sol index e129223a..b023c09c 100644 --- a/test/token/Staking/Staking.t.sol +++ b/test/token/Staking/Staking.t.sol @@ -104,4 +104,43 @@ contract StakingTest is Test { assertEq(minStakeAmount, MIN_STAKE_AMOUNT); assertEq(maxStakeAmount, MAX_STAKE_AMOUNT); } + + function test_StakeERC20UpdatesState() public { + uint256 stakeAmount = 100 ether; + + // Stake ERC-20 tokens + staking.stakeERC20(address(erc20Token), stakeAmount); + + (uint256 amount,,,) = staking.getStakedTokenInfo(address(erc20Token), 0); + + assertEq(amount, stakeAmount); + } + + function test_StakeERC721UpdatesState() public { + // Mint an ERC-721 token to Alice + erc721Token.mint(alice, TOKEN_ID_1); + + // Stake ERC-721 token + vm.prank(alice); + staking.stakeERC721(address(erc721Token), TOKEN_ID_1); + + (uint256 amount,,,) = staking.getStakedTokenInfo(address(erc721Token), TOKEN_ID_1); + + assertEq(amount, 1); + } + + function test_StakeERC1155UpdatesState() public { + uint256 stakeAmount = 10; + + // Mint ERC-1155 tokens to Bob + erc1155Token.mint(bob, TOKEN_ID_2, stakeAmount); + + // Stake ERC-1155 tokens + vm.prank(bob); + staking.stakeERC1155(address(erc1155Token), TOKEN_ID_2, stakeAmount); + + (uint256 amount,,,) = staking.getStakedTokenInfo(address(erc1155Token), TOKEN_ID_2); + + assertEq(amount, stakeAmount); + } } diff --git a/test/token/Staking/harnesses/LibStakingHarness.sol b/test/token/Staking/harnesses/LibStakingHarness.sol index 9690e340..68ffcd50 100644 --- a/test/token/Staking/harnesses/LibStakingHarness.sol +++ b/test/token/Staking/harnesses/LibStakingHarness.sol @@ -64,4 +64,15 @@ contract LibStakingHarness { { return StakingMod.getStakingParameters(); } + + /** + * @notice Exposes StakingMod.getStakedTokenInfo as an external function + */ + function getStakedTokenInfo(address _tokenAddress, uint256 _tokenId) + external + view + returns (uint256, uint256, uint256, uint256) + { + return StakingMod.getStakedTokenInfo(_tokenAddress, _tokenId); + } } From e6180abd5620aa3b5b2be4666d38dc1bce1d1706 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Thu, 18 Dec 2025 23:37:34 +0800 Subject: [PATCH 07/20] added events and security enhancement --- src/token/Staking/StakingFacet.sol | 128 ++++++++++++++++-- src/token/Staking/StakingMod.sol | 67 ++++++++- .../Staking/harnesses/LibStakingHarness.sol | 14 ++ 3 files changed, 195 insertions(+), 14 deletions(-) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index e347f6ed..57fc6f13 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -108,11 +108,6 @@ interface IERC1155Receiver { * @dev Implements staking, unstaking, and reward claiming functionalities using diamond storage. */ contract StakingFacet { - /** - * @title LibStaking - Standard Staking Library - * @notice Provides internal functions and storage layout for staking ERC-20, ERC-721, and ERC-1155 tokens. - * @dev Uses ERC-8042 for storage location standardization. - */ /** * @notice Thrown when attempting to stake an unsupported token type. * @param tokenAddress The address of the unsupported token. @@ -189,6 +184,31 @@ contract StakingFacet { */ error StakingInsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + /** + * @notice Emitted when staking parameters are updated. + * @param baseAPR The base annual percentage rate for rewards. + * @param rewardDecayRate The decay rate for rewards over time. + * @param compoundFrequency The frequency at which rewards are compounded. + * @param rewardToken The address of the token used for rewards. + * @param cooldownPeriod The cooldown period before unstaking is allowed. + * @param minStakeAmount The minimum amount required to stake. + * @param maxStakeAmount The maximum amount allowed to stake. + */ + event StakingParametersUpdated( + uint256 baseAPR, + uint256 rewardDecayRate, + uint256 compoundFrequency, + address rewardToken, + uint256 cooldownPeriod, + uint256 minStakeAmount, + uint256 maxStakeAmount + ); + + /** + * @notice Emitted when supported token types are added. + */ + event SupportedTokenAdded(address indexed tokenAddress, bool isERC20, bool isERC721, bool isERC1155); + /** * @notice Emitted when tokens are successfully staked. * @param staker The address of the user who staked the tokens. @@ -314,14 +334,16 @@ contract StakingFacet { if (s.supportedTokens[_tokenAddress].isERC20) { IERC20(_tokenAddress).transferFrom(msg.sender, address(this), _amount); - _stakeERC20(_tokenAddress, _amount); + stakeERC20(_tokenAddress, _amount); } else if (s.supportedTokens[_tokenAddress].isERC721) { IERC721(_tokenAddress).safeTransferFrom(msg.sender, address(this), _tokenId); - _stakeERC721(_tokenAddress, _tokenId); + stakeERC721(_tokenAddress, _tokenId); } else if (s.supportedTokens[_tokenAddress].isERC1155) { IERC1155(_tokenAddress).safeTransferFrom(msg.sender, address(this), _tokenId, _amount, ""); - _stakeERC1155(_tokenAddress, _tokenId, _amount); + stakeERC1155(_tokenAddress, _tokenId, _amount); } + + emit TokensStaked(msg.sender, _tokenAddress, _tokenId, _amount); } /** @@ -356,9 +378,10 @@ contract StakingFacet { } s.totalStakedPerToken[_tokenAddress] -= amount; - delete s.stakedTokens[_tokenAddress][_tokenId]; emit TokensUnstaked(msg.sender, _tokenAddress, _tokenId, amount); + + delete s.stakedTokens[_tokenAddress][_tokenId]; } /** @@ -369,9 +392,14 @@ contract StakingFacet { * @param _isERC1155 Boolean indicating if the token is ERC-1155 * @dev This function should be restricted to admin use only. */ - function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) external { + function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) + external + returns (bool) + { StakingStorage storage s = getStorage(); s.supportedTokens[_tokenAddress] = TokenType({isERC20: _isERC20, isERC721: _isERC721, isERC1155: _isERC1155}); + emit SupportedTokenAdded(_tokenAddress, _isERC20, _isERC721, _isERC1155); + return true; } /** @@ -395,6 +423,16 @@ contract StakingFacet { uint256 _maxStakeAmount ) external { StakingStorage storage s = getStorage(); + + bool isSupported = isTokenSupported(_rewardToken); + if (!isSupported) { + revert StakingUnsupportedToken(_rewardToken); + } + + if (_minStakeAmount == 0 || _maxStakeAmount == 0) { + revert StakingZeroStakeAmount(); + } + s.baseAPR = _baseAPR; s.rewardDecayRate = _rewardDecayRate; s.compoundFrequency = _compoundFrequency; @@ -402,6 +440,70 @@ contract StakingFacet { s.cooldownPeriod = _cooldownPeriod; s.minStakeAmount = _minStakeAmount; s.maxStakeAmount = _maxStakeAmount; + + emit StakingParametersUpdated( + _baseAPR, + _rewardDecayRate, + _compoundFrequency, + _rewardToken, + _cooldownPeriod, + _minStakeAmount, + _maxStakeAmount + ); + } + + /** + * @notice Retrieve staking parameters + * @return baseAPR The base annual percentage rate for rewards. + * @return rewardDecayRate The decay rate for rewards over time. + * @return compoundFrequency The frequency at which rewards are compounded. + * @return rewardToken The address of the token used for rewards. + * @return cooldownPeriod The cooldown period before unstaking is allowed. + * @return minStakeAmount The minimum amount required to stake. + * @return maxStakeAmount The maximum amount allowed to stake. + */ + function getStakingParameters() + external + view + returns ( + uint256 baseAPR, + uint256 rewardDecayRate, + uint256 compoundFrequency, + address rewardToken, + uint256 cooldownPeriod, + uint256 minStakeAmount, + uint256 maxStakeAmount + ) + { + StakingStorage storage s = getStorage(); + return ( + s.baseAPR, + s.rewardDecayRate, + s.compoundFrequency, + s.rewardToken, + s.cooldownPeriod, + s.minStakeAmount, + s.maxStakeAmount + ); + } + + /** + * @notice Retrieve staked token info for ERC-20, ERC-721, or ERC-1155 tokens + * @param _tokenAddress The address of the token. + * @param _tokenId The ID of the token (0 for ERC-20). + * @return amount The amount of tokens staked. + * @return stakedAt The timestamp when the tokens were staked. + * @return lastClaimedAt The timestamp when rewards were last claimed. + * @return accumulatedRewards The total accumulated rewards for the staked tokens. + */ + function getStakedTokenInfo(address _tokenAddress, uint256 _tokenId) + external + view + returns (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) + { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + return (stake.amount, stake.stakedAt, stake.lastClaimedAt, stake.accumulatedRewards); } /** @@ -420,7 +522,7 @@ contract StakingFacet { * @param _tokenAddress The address of the ERC-20 token to stake. * @param _value The amount of tokens to stake. */ - function _stakeERC20(address _tokenAddress, uint256 _value) internal { + function stakeERC20(address _tokenAddress, uint256 _value) internal { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][0]; @@ -437,7 +539,7 @@ contract StakingFacet { * @param _tokenAddress The address of the ERC-721 token to stake. * @param _tokenId The ID of the token to stake. */ - function _stakeERC721(address _tokenAddress, uint256 _tokenId) internal { + function stakeERC721(address _tokenAddress, uint256 _tokenId) internal { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; @@ -455,7 +557,7 @@ contract StakingFacet { * @param _tokenId The ID of the token to stake. * @param _value The amount of tokens to stake. */ - function _stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) internal { + function stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) internal { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol index 6f2c956a..e047043d 100644 --- a/src/token/Staking/StakingMod.sol +++ b/src/token/Staking/StakingMod.sol @@ -8,6 +8,42 @@ pragma solidity >=0.8.30; * @dev Uses ERC-8042 for storage location standardization. */ +/** + * @notice Thrown when attempting to stake an unsupported token type. + * @param tokenAddress The address of the unsupported token. + */ +error StakingUnsupportedToken(address tokenAddress); + +/** + * @notice Thrown when attempting to stake a zero amount. + */ +error StakingZeroStakeAmount(); + +/** + * @notice Emitted when staking parameters are updated. + * @param baseAPR The base annual percentage rate for rewards. + * @param rewardDecayRate The decay rate for rewards over time. + * @param compoundFrequency The frequency at which rewards are compounded. + * @param rewardToken The address of the token used for rewards. + * @param cooldownPeriod The cooldown period before unstaking is allowed. + * @param minStakeAmount The minimum amount required to stake. + * @param maxStakeAmount The maximum amount allowed to stake. + */ +event StakingParametersUpdated( + uint256 baseAPR, + uint256 rewardDecayRate, + uint256 compoundFrequency, + address rewardToken, + uint256 cooldownPeriod, + uint256 minStakeAmount, + uint256 maxStakeAmount +); + +/** + * @notice Emitted when supported token types are added. + */ +event SupportedTokenAdded(address indexed tokenAddress, bool isERC20, bool isERC721, bool isERC1155); + /** * @dev Storage position constant defined via keccak256 hash of diamond storage identifier. */ @@ -76,9 +112,13 @@ function getStorage() pure returns (StakingStorage storage s) { * @param _isERC1155 Boolean indicating if the token is ERC-1155 * @dev This function should be restricted to admin use only. */ -function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) { +function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) returns (bool) { StakingStorage storage s = getStorage(); s.supportedTokens[_tokenAddress] = TokenType({isERC20: _isERC20, isERC721: _isERC721, isERC1155: _isERC1155}); + + emit SupportedTokenAdded(_tokenAddress, _isERC20, _isERC721, _isERC1155); + + return true; } /** @@ -102,6 +142,16 @@ function setStakingParameters( uint256 _maxStakeAmount ) { StakingStorage storage s = getStorage(); + + bool isSupported = isTokenSupported(_rewardToken); + if (!isSupported) { + revert StakingUnsupportedToken(_rewardToken); + } + + if (_minStakeAmount == 0 || _maxStakeAmount == 0) { + revert StakingZeroStakeAmount(); + } + s.baseAPR = _baseAPR; s.rewardDecayRate = _rewardDecayRate; s.compoundFrequency = _compoundFrequency; @@ -109,6 +159,10 @@ function setStakingParameters( s.cooldownPeriod = _cooldownPeriod; s.minStakeAmount = _minStakeAmount; s.maxStakeAmount = _maxStakeAmount; + + emit StakingParametersUpdated( + _baseAPR, _rewardDecayRate, _compoundFrequency, _rewardToken, _cooldownPeriod, _minStakeAmount, _maxStakeAmount + ); } /** @@ -214,3 +268,14 @@ function getStakedTokenInfo(address _tokenAddress, uint256 _tokenId) StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; return (stake.amount, stake.stakedAt, stake.lastClaimedAt, stake.accumulatedRewards); } + +/** + * @notice Retrieve supported token types + * @param _tokenAddress The address of the token. + * @return true if the token is supported, false otherwise. + */ +function isTokenSupported(address _tokenAddress) view returns (bool) { + StakingStorage storage s = getStorage(); + TokenType storage tokenType = s.supportedTokens[_tokenAddress]; + return tokenType.isERC20 || tokenType.isERC721 || tokenType.isERC1155; +} diff --git a/test/token/Staking/harnesses/LibStakingHarness.sol b/test/token/Staking/harnesses/LibStakingHarness.sol index 68ffcd50..fb0094d7 100644 --- a/test/token/Staking/harnesses/LibStakingHarness.sol +++ b/test/token/Staking/harnesses/LibStakingHarness.sol @@ -33,6 +33,13 @@ contract LibStakingHarness { ); } + /** + * @notice Exposes StakingMod.addSupportedToken as an external function + */ + function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) external { + StakingMod.addSupportedToken(_tokenAddress, _isERC20, _isERC721, _isERC1155); + } + /** * @notice Exposes StakingMod._stakeERC20 as an external function */ @@ -75,4 +82,11 @@ contract LibStakingHarness { { return StakingMod.getStakedTokenInfo(_tokenAddress, _tokenId); } + + /** + * @notice Exposes StakingMod.isSupportedToken as an external function + */ + function isSupportedToken(address _tokenAddress) external view returns (bool) { + return StakingMod.isTokenSupported(_tokenAddress); + } } From d9d49aa4260bfba21d39b496782fb6f0b08a7f80 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Fri, 19 Dec 2025 10:52:56 +0800 Subject: [PATCH 08/20] LibStaking test updated --- src/token/Staking/StakingFacet.sol | 15 +- src/token/Staking/StakingMod.sol | 17 +++ test/token/Staking/Staking.t.sol | 132 ++++++++++++++++++ .../Staking/harnesses/LibStakingHarness.sol | 2 +- 4 files changed, 162 insertions(+), 4 deletions(-) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index 57fc6f13..d7150550 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -322,9 +322,7 @@ contract StakingFacet { StakingStorage storage s = getStorage(); TokenType storage tokenType = s.supportedTokens[_tokenAddress]; - if (!tokenType.isERC20 && !tokenType.isERC721 && !tokenType.isERC1155) { - revert StakingUnsupportedToken(_tokenAddress); - } + if (_amount < s.minStakeAmount) { revert StakingAmountBelowMinimum(_amount, s.minStakeAmount); } @@ -617,6 +615,17 @@ contract StakingFacet { emit RewardsClaimed(msg.sender, _tokenAddress, _tokenId, rewards); } + /** + * @notice Retrieve supported token types + * @param _tokenAddress The address of the token. + * @return true if the token is supported, false otherwise. + */ + function isTokenSupported(address _tokenAddress) internal view returns (bool) { + StakingStorage storage s = getStorage(); + TokenType storage tokenType = s.supportedTokens[_tokenAddress]; + return true; + } + /** * @notice Support Interface to satisfy ERC-165 standard. * @param interfaceId The interface identifier, as specified in ERC-165. diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol index e047043d..85d46192 100644 --- a/src/token/Staking/StakingMod.sol +++ b/src/token/Staking/StakingMod.sol @@ -111,6 +111,7 @@ function getStorage() pure returns (StakingStorage storage s) { * @param _isERC721 Boolean indicating if the token is ERC-721. * @param _isERC1155 Boolean indicating if the token is ERC-1155 * @dev This function should be restricted to admin use only. + * @dev Emits a SupportedTokenAdded event upon successful addition. */ function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) returns (bool) { StakingStorage storage s = getStorage(); @@ -131,6 +132,7 @@ function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, * @param _minStakeAmount The minimum amount required to stake. * @param _maxStakeAmount The maximum amount allowed to stake. * @dev This function should be restricted to admin use only. + * @dev Emits a StakingParametersUpdated event upon successful update. */ function setStakingParameters( uint256 _baseAPR, @@ -175,6 +177,11 @@ function stakeERC20(address _tokenAddress, uint256 _value) { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][0]; + bool isSupported = isTokenSupported(_tokenAddress); + if (!isSupported) { + revert StakingUnsupportedToken(_tokenAddress); + } + stake.amount += _value; stake.stakedAt = block.timestamp; stake.lastClaimedAt = block.timestamp; @@ -192,6 +199,11 @@ function stakeERC721(address _tokenAddress, uint256 _tokenId) { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + bool isSupported = isTokenSupported(_tokenAddress); + if (!isSupported) { + revert StakingUnsupportedToken(_tokenAddress); + } + stake.amount = 1; stake.stakedAt = block.timestamp; stake.lastClaimedAt = block.timestamp; @@ -210,6 +222,11 @@ function stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + bool isSupported = isTokenSupported(_tokenAddress); + if (!isSupported) { + revert StakingUnsupportedToken(_tokenAddress); + } + stake.amount += _value; stake.stakedAt = block.timestamp; stake.lastClaimedAt = block.timestamp; diff --git a/test/token/Staking/Staking.t.sol b/test/token/Staking/Staking.t.sol index b023c09c..d9c68d6f 100644 --- a/test/token/Staking/Staking.t.sol +++ b/test/token/Staking/Staking.t.sol @@ -6,6 +6,7 @@ import {LibStakingHarness} from "./harnesses/LibStakingHarness.sol"; import {ERC20FacetHarness} from "../ERC20//ERC20/harnesses/ERC20FacetHarness.sol"; import {ERC721FacetHarness} from "../ERC721/ERC721/harnesses/ERC721FacetHarness.sol"; import {ERC1155FacetHarness} from "../ERC1155/ERC1155/harnesses/ERC1155FacetHarness.sol"; +import "../../../../src/token/Staking/StakingMod.sol" as StakingMod; // import "forge-std/console.sol"; @@ -42,6 +43,18 @@ contract StakingTest is Test { uint256 constant MIN_STAKE_AMOUNT = 1 ether; uint256 constant MAX_STAKE_AMOUNT = 1000 ether; + event StakingParametersUpdated( + uint256 baseAPR, + uint256 rewardDecayRate, + uint256 compoundFrequency, + address rewardToken, + uint256 cooldownPeriod, + uint256 minStakeAmount, + uint256 maxStakeAmount + ); + + event SupportedTokenAdded(address indexed tokenAddress, bool isERC20, bool isERC721, bool isERC1155); + function setUp() public { alice = makeAddr("alice"); bob = makeAddr("bob"); @@ -74,6 +87,18 @@ contract StakingTest is Test { * Deploy and initialize the staking harness */ staking = new LibStakingHarness(); + + /** + * Register supported tokens + */ + staking.addSupportedToken(address(erc20Token), true, false, false); + staking.addSupportedToken(address(erc721Token), false, true, false); + staking.addSupportedToken(address(erc1155Token), false, false, true); + staking.addSupportedToken(address(rewardToken), true, false, false); + + /** + * Initialize staking parameters + */ staking.initialize( BASE_APR, REWARD_DECAY_RATE, @@ -105,6 +130,106 @@ contract StakingTest is Test { assertEq(maxStakeAmount, MAX_STAKE_AMOUNT); } + function test_ParametersAreSetCorrectly_EventEmitted() public { + vm.expectEmit(true, true, true, true); + emit StakingParametersUpdated( + BASE_APR, + REWARD_DECAY_RATE, + COMPOUND_FREQUENCY, + address(rewardToken), + COOLDOWN_PERIOD, + MIN_STAKE_AMOUNT, + MAX_STAKE_AMOUNT + ); + + staking.initialize( + BASE_APR, + REWARD_DECAY_RATE, + COMPOUND_FREQUENCY, + address(rewardToken), + COOLDOWN_PERIOD, + MIN_STAKE_AMOUNT, + MAX_STAKE_AMOUNT + ); + } + + function test_ParametersAreSetCorrectly_RevertOnUnsupportedToken() public { + address unsupportedToken = makeAddr("unsupportedToken"); + + vm.expectRevert(abi.encodeWithSelector(StakingMod.StakingUnsupportedToken.selector, unsupportedToken)); + staking.initialize( + BASE_APR, + REWARD_DECAY_RATE, + COMPOUND_FREQUENCY, + unsupportedToken, + COOLDOWN_PERIOD, + MIN_STAKE_AMOUNT, + MAX_STAKE_AMOUNT + ); + } + + function test_ParametersAreSetCorrectly_RevertOnZeroStakeAmount() public { + uint256 newMinStakeAmount = 0; + vm.expectRevert(abi.encodeWithSelector(StakingMod.StakingZeroStakeAmount.selector, newMinStakeAmount)); + staking.initialize( + BASE_APR, + REWARD_DECAY_RATE, + COMPOUND_FREQUENCY, + address(rewardToken), + COOLDOWN_PERIOD, + newMinStakeAmount, + MAX_STAKE_AMOUNT + ); + + uint256 newMaxStakeAmount = 0; + vm.expectRevert(abi.encodeWithSelector(StakingMod.StakingZeroStakeAmount.selector, newMaxStakeAmount)); + staking.initialize( + BASE_APR, + REWARD_DECAY_RATE, + COMPOUND_FREQUENCY, + address(rewardToken), + COOLDOWN_PERIOD, + MIN_STAKE_AMOUNT, + newMaxStakeAmount + ); + } + + function test_ParametersAreSetCorrectly_EmitEventsSupportedToken() public { + vm.expectEmit(true, true, true, true); + emit SupportedTokenAdded(address(erc20Token), true, false, false); + staking.addSupportedToken(address(erc20Token), true, false, false); + + vm.expectEmit(true, true, true, true); + emit SupportedTokenAdded(address(erc721Token), false, true, false); + staking.addSupportedToken(address(erc721Token), false, true, false); + + vm.expectEmit(true, true, true, true); + emit SupportedTokenAdded(address(erc1155Token), false, false, true); + staking.addSupportedToken(address(erc1155Token), false, false, true); + + vm.expectEmit(true, true, true, true); + emit SupportedTokenAdded(address(rewardToken), true, false, false); + staking.addSupportedToken(address(rewardToken), true, false, false); + } + + function test_VerifySupportedTokens() public { + // Supported tokens + bool isERC20Supported = staking.isTokenSupported(address(erc20Token)); + bool isERC721Supported = staking.isTokenSupported(address(erc721Token)); + bool isERC1155Supported = staking.isTokenSupported(address(erc1155Token)); + bool isRewardTokenSupported = staking.isTokenSupported(address(rewardToken)); + + assertTrue(isERC20Supported); + assertTrue(isERC721Supported); + assertTrue(isERC1155Supported); + assertTrue(isRewardTokenSupported); + + // Unsupported token + address unsupportedToken = makeAddr("unsupportedToken"); + bool isUnsupportedTokenSupported = staking.isTokenSupported(unsupportedToken); + assertFalse(isUnsupportedTokenSupported); + } + function test_StakeERC20UpdatesState() public { uint256 stakeAmount = 100 ether; @@ -143,4 +268,11 @@ contract StakingTest is Test { assertEq(amount, stakeAmount); } + + function test_StakeTokens_RevertWithUnsupportedToken() public { + address unsupportedToken = makeAddr("unsupportedToken"); + + vm.expectRevert(abi.encodeWithSelector(StakingMod.StakingUnsupportedToken.selector, unsupportedToken)); + staking.stakeERC20(unsupportedToken, 100 ether); + } } diff --git a/test/token/Staking/harnesses/LibStakingHarness.sol b/test/token/Staking/harnesses/LibStakingHarness.sol index fb0094d7..f58e9c78 100644 --- a/test/token/Staking/harnesses/LibStakingHarness.sol +++ b/test/token/Staking/harnesses/LibStakingHarness.sol @@ -86,7 +86,7 @@ contract LibStakingHarness { /** * @notice Exposes StakingMod.isSupportedToken as an external function */ - function isSupportedToken(address _tokenAddress) external view returns (bool) { + function isTokenSupported(address _tokenAddress) external view returns (bool) { return StakingMod.isTokenSupported(_tokenAddress); } } From 4802e10457c6a47e693f9e48c89303fd4c2e2084 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Fri, 19 Dec 2025 16:18:39 +0800 Subject: [PATCH 09/20] condition sets in StakingFacet --- src/token/Staking/StakingFacet.sol | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index d7150550..65f35674 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -524,6 +524,11 @@ contract StakingFacet { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][0]; + bool isSupported = isTokenSupported(_tokenAddress); + if (!isSupported) { + revert StakingUnsupportedToken(_tokenAddress); + } + stake.amount += _value; stake.stakedAt = block.timestamp; stake.lastClaimedAt = block.timestamp; @@ -541,6 +546,11 @@ contract StakingFacet { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + bool isSupported = isTokenSupported(_tokenAddress); + if (!isSupported) { + revert StakingUnsupportedToken(_tokenAddress); + } + stake.amount = 1; stake.stakedAt = block.timestamp; stake.lastClaimedAt = block.timestamp; @@ -559,6 +569,11 @@ contract StakingFacet { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + bool isSupported = isTokenSupported(_tokenAddress); + if (!isSupported) { + revert StakingUnsupportedToken(_tokenAddress); + } + stake.amount += _value; stake.stakedAt = block.timestamp; stake.lastClaimedAt = block.timestamp; From 04fbcd6b39a455f61d2245af5637a23355d77559 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Sat, 20 Dec 2025 12:41:02 +0800 Subject: [PATCH 10/20] revise mapping for StakingFacet --- src/token/Staking/StakingFacet.sol | 75 +++-- src/token/Staking/StakingMod.sol | 12 +- test/token/Staking/Staking.t.sol | 7 +- test/token/Staking/StakingFacet.t.sol | 278 ++++++++++++++++++ .../Staking/harnesses/StakingFacetHarness.sol | 57 +++- 5 files changed, 401 insertions(+), 28 deletions(-) create mode 100644 test/token/Staking/StakingFacet.t.sol diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index 65f35674..8ca50d16 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -288,7 +288,8 @@ contract StakingFacet { */ struct StakingStorage { mapping(address tokenType => TokenType) supportedTokens; - mapping(address tokenAddress => mapping(uint256 tokenId => StakedTokenInfo)) stakedTokens; + mapping(address tokenOwner => mapping(address tokenAddress => mapping(uint256 tokenId => StakedTokenInfo))) + stakedTokens; uint256 baseAPR; uint256 rewardDecayRate; uint256 compoundFrequency; @@ -297,7 +298,6 @@ contract StakingFacet { uint256 cooldownPeriod; uint256 maxStakeAmount; uint256 minStakeAmount; - mapping(address user => mapping(address spender => uint256 allowance)) allowance; } /** @@ -320,23 +320,39 @@ contract StakingFacet { */ function stakeToken(address _tokenAddress, uint256 _tokenId, uint256 _amount) external { StakingStorage storage s = getStorage(); - TokenType storage tokenType = s.supportedTokens[_tokenAddress]; - if (_amount < s.minStakeAmount) { - revert StakingAmountBelowMinimum(_amount, s.minStakeAmount); + bool isSupported = isTokenSupported(_tokenAddress); + if (!isSupported) { + revert StakingUnsupportedToken(_tokenAddress); + } + + if (s.minStakeAmount > 0) { + if (_amount <= s.minStakeAmount) { + revert StakingAmountBelowMinimum(_amount, s.minStakeAmount); + } } - if (_amount > s.maxStakeAmount) { - revert StakingAmountAboveMaximum(_amount, s.maxStakeAmount); + if (s.maxStakeAmount > 0) { + if (_amount + s.totalStakedPerToken[_tokenAddress] >= s.maxStakeAmount) { + revert StakingAmountAboveMaximum(_amount, s.maxStakeAmount); + } } if (s.supportedTokens[_tokenAddress].isERC20) { IERC20(_tokenAddress).transferFrom(msg.sender, address(this), _amount); stakeERC20(_tokenAddress, _amount); } else if (s.supportedTokens[_tokenAddress].isERC721) { + if (IERC721(_tokenAddress).ownerOf(_tokenId) != msg.sender) { + revert StakingNotTokenOwner(msg.sender, _tokenAddress, _tokenId); + } IERC721(_tokenAddress).safeTransferFrom(msg.sender, address(this), _tokenId); stakeERC721(_tokenAddress, _tokenId); } else if (s.supportedTokens[_tokenAddress].isERC1155) { + if (IERC1155(_tokenAddress).balanceOf(msg.sender, _tokenId) < _amount) { + revert StakingInsufficientBalance( + msg.sender, IERC1155(_tokenAddress).balanceOf(msg.sender, _tokenId), _amount + ); + } IERC1155(_tokenAddress).safeTransferFrom(msg.sender, address(this), _tokenId, _amount, ""); stakeERC1155(_tokenAddress, _tokenId, _amount); } @@ -351,7 +367,7 @@ contract StakingFacet { */ function unstakeToken(address _tokenAddress, uint256 _tokenId) external { StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; uint256 amount = stake.amount; if (amount == 0) { @@ -379,7 +395,7 @@ contract StakingFacet { emit TokensUnstaked(msg.sender, _tokenAddress, _tokenId, amount); - delete s.stakedTokens[_tokenAddress][_tokenId]; + delete s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; } /** @@ -500,7 +516,7 @@ contract StakingFacet { returns (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) { StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; return (stake.amount, stake.stakedAt, stake.lastClaimedAt, stake.accumulatedRewards); } @@ -522,7 +538,7 @@ contract StakingFacet { */ function stakeERC20(address _tokenAddress, uint256 _value) internal { StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][0]; + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][0]; bool isSupported = isTokenSupported(_tokenAddress); if (!isSupported) { @@ -544,7 +560,7 @@ contract StakingFacet { */ function stakeERC721(address _tokenAddress, uint256 _tokenId) internal { StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; bool isSupported = isTokenSupported(_tokenAddress); if (!isSupported) { @@ -567,7 +583,7 @@ contract StakingFacet { */ function stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) internal { StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; bool isSupported = isTokenSupported(_tokenAddress); if (!isSupported) { @@ -590,15 +606,18 @@ contract StakingFacet { */ function calculateRewards(address _tokenAddress, uint256 _tokenId) internal view returns (uint256) { StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; + // Calculate staking duration uint256 stakedDuration = block.timestamp - stake.lastClaimedAt; if (stakedDuration == 0 || stake.amount == 0) { return 0; } + // Base reward rate with decay uint256 baseReward = (stake.amount * s.baseAPR * stakedDuration) / (365 days * 100); + // Apply decay factor based on staking duration and compound frequency uint256 decayFactor = s.rewardDecayRate > 0 ? (s.rewardDecayRate ** (stakedDuration / s.compoundFrequency)) : 10 ** 18; @@ -615,11 +634,11 @@ contract StakingFacet { */ function _claimRewards(address _tokenAddress, uint256 _tokenId) internal { StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; uint256 rewards = calculateRewards(_tokenAddress, _tokenId); if (rewards == 0) { - return; + revert StakingNoRewardsToClaim(_tokenAddress, _tokenId); } IERC20(s.rewardToken).transfer(msg.sender, rewards); @@ -638,7 +657,7 @@ contract StakingFacet { function isTokenSupported(address _tokenAddress) internal view returns (bool) { StakingStorage storage s = getStorage(); TokenType storage tokenType = s.supportedTokens[_tokenAddress]; - return true; + return tokenType.isERC20 || tokenType.isERC721 || tokenType.isERC1155; } /** @@ -649,4 +668,26 @@ contract StakingFacet { function supportsInterface(bytes4 interfaceId) external pure returns (bool) { return interfaceId == type(IERC721Receiver).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId; } + + /// @notice Handle the receipt of an NFT + /// @dev The ERC721 smart contract calls this on the recipient after a `safeTransfer`. + /// @return The selector to confirm token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) + external + pure + returns (bytes4) + { + return IERC721Receiver.onERC721Received.selector; + } + + /// @notice Handle the receipt of a single ERC1155 token type + /// @dev The ERC1155 smart contract calls this on the recipient after a `safeTransferFrom`. + /// @return The selector to confirm token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes calldata data) + external + pure + returns (bytes4) + { + return IERC1155Receiver.onERC1155Received.selector; + } } diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol index 85d46192..1905fb91 100644 --- a/src/token/Staking/StakingMod.sol +++ b/src/token/Staking/StakingMod.sol @@ -80,7 +80,8 @@ struct TokenType { */ struct StakingStorage { mapping(address tokenType => TokenType) supportedTokens; - mapping(address tokenAddress => mapping(uint256 tokenId => StakedTokenInfo)) stakedTokens; + mapping(address tokenOwner => mapping(address tokenAddress => mapping(uint256 tokenId => StakedTokenInfo))) + stakedTokens; uint256 baseAPR; uint256 rewardDecayRate; uint256 compoundFrequency; @@ -89,7 +90,6 @@ struct StakingStorage { uint256 cooldownPeriod; uint256 maxStakeAmount; uint256 minStakeAmount; - mapping(address user => mapping(address spender => uint256 allowance)) allowance; } /** @@ -175,7 +175,7 @@ function setStakingParameters( */ function stakeERC20(address _tokenAddress, uint256 _value) { StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][0]; + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][0]; bool isSupported = isTokenSupported(_tokenAddress); if (!isSupported) { @@ -197,7 +197,7 @@ function stakeERC20(address _tokenAddress, uint256 _value) { */ function stakeERC721(address _tokenAddress, uint256 _tokenId) { StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; bool isSupported = isTokenSupported(_tokenAddress); if (!isSupported) { @@ -220,7 +220,7 @@ function stakeERC721(address _tokenAddress, uint256 _tokenId) { */ function stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) { StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; bool isSupported = isTokenSupported(_tokenAddress); if (!isSupported) { @@ -282,7 +282,7 @@ function getStakedTokenInfo(address _tokenAddress, uint256 _tokenId) returns (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) { StakingStorage storage s = getStorage(); - StakedTokenInfo storage stake = s.stakedTokens[_tokenAddress][_tokenId]; + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; return (stake.amount, stake.stakedAt, stake.lastClaimedAt, stake.accumulatedRewards); } diff --git a/test/token/Staking/Staking.t.sol b/test/token/Staking/Staking.t.sol index d9c68d6f..d8080381 100644 --- a/test/token/Staking/Staking.t.sol +++ b/test/token/Staking/Staking.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.30; -import {Test} from "forge-std/Test.sol"; +import {Test, console} from "forge-std/Test.sol"; import {LibStakingHarness} from "./harnesses/LibStakingHarness.sol"; import {ERC20FacetHarness} from "../ERC20//ERC20/harnesses/ERC20FacetHarness.sol"; import {ERC721FacetHarness} from "../ERC721/ERC721/harnesses/ERC721FacetHarness.sol"; @@ -29,12 +29,9 @@ contract StakingTest is Test { string constant BASE_URI = "https://example.com/api/nft/"; string constant DEFAULT_URI = "https://token.uri/{id}.json"; - string constant BASE_URI_1155 = "https://base.uri/"; - string constant TOKEN_URI = "token1.json"; uint256 constant TOKEN_ID_1 = 1; uint256 constant TOKEN_ID_2 = 2; - uint256 constant TOKEN_ID_3 = 3; uint256 constant BASE_APR = 500; // 5% uint256 constant REWARD_DECAY_RATE = 50; // 0.5% @@ -250,6 +247,7 @@ contract StakingTest is Test { staking.stakeERC721(address(erc721Token), TOKEN_ID_1); (uint256 amount,,,) = staking.getStakedTokenInfo(address(erc721Token), TOKEN_ID_1); + console.log("Staked amount:", amount); assertEq(amount, 1); } @@ -265,6 +263,7 @@ contract StakingTest is Test { staking.stakeERC1155(address(erc1155Token), TOKEN_ID_2, stakeAmount); (uint256 amount,,,) = staking.getStakedTokenInfo(address(erc1155Token), TOKEN_ID_2); + console.log("Staked amount:", amount); assertEq(amount, stakeAmount); } diff --git a/test/token/Staking/StakingFacet.t.sol b/test/token/Staking/StakingFacet.t.sol new file mode 100644 index 00000000..e857d022 --- /dev/null +++ b/test/token/Staking/StakingFacet.t.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.30; + +import {Test} from "forge-std/Test.sol"; +import {StakingFacetHarness} from "./harnesses/StakingFacetHarness.sol"; +import {StakingFacet} from "../../../src/token/Staking/StakingFacet.sol"; +import {ERC20FacetHarness} from "../ERC20//ERC20/harnesses/ERC20FacetHarness.sol"; +import {ERC721FacetHarness} from "../ERC721/ERC721/harnesses/ERC721FacetHarness.sol"; +import {ERC1155FacetHarness} from "../ERC1155/ERC1155/harnesses/ERC1155FacetHarness.sol"; +import {IERC721Receiver} from "../../../src/interfaces/IERC721Receiver.sol"; +import {IERC1155Receiver} from "../../../src/interfaces/IERC1155Receiver.sol"; + +contract StakingFacetTest is Test { + StakingFacetHarness public facet; + ERC20FacetHarness erc20Token; + ERC20FacetHarness rewardToken; + ERC721FacetHarness erc721Token; + ERC1155FacetHarness erc1155Token; + + address public alice; + address public bob; + address public owner; + + string constant ASSET_NAME = "Test Token"; + string constant ASSET_SYMBOL = "TEST"; + uint8 constant ASSET_DECIMALS = 18; + + string constant TOKEN_NAME = "Test Token"; + string constant TOKEN_SYMBOL = "TEST"; + string constant BASE_URI = "https://example.com/api/nft/"; + + string constant DEFAULT_URI = "https://token.uri/{id}.json"; + + uint256 constant TOKEN_ID_1 = 1; + uint256 constant TOKEN_ID_2 = 2; + + uint256 constant BASE_APR = 10; // 10% + uint256 constant REWARD_DECAY_RATE = 0; // no decay + uint256 constant COMPOUND_FREQUENCY = 365 days; + uint256 constant COOLDOWN_PERIOD = 1 days; + uint256 constant MIN_STAKE_AMOUNT = 0 ether; + uint256 constant MAX_STAKE_AMOUNT = 1_000_000 ether; + + function setUp() public { + alice = makeAddr("alice"); + bob = makeAddr("bob"); + owner = makeAddr("owner"); + + vm.startPrank(owner); + + /** + * Deploy and initialize the reward ERC-20 token + */ + rewardToken = new ERC20FacetHarness(); + rewardToken.initialize("Reward Token", "RWD", 18); + + /** + * Deploy and initialize the ERC-20 token for staking tests + */ + erc20Token = new ERC20FacetHarness(); + erc20Token.initialize(ASSET_NAME, ASSET_SYMBOL, ASSET_DECIMALS); + + /** + * Deploy and initialize the ERC-721 token for staking tests + */ + erc721Token = new ERC721FacetHarness(); + erc721Token.initialize(TOKEN_NAME, TOKEN_SYMBOL, BASE_URI); + + /** + * Deploy and initialize the ERC-1155 token for staking tests + */ + erc1155Token = new ERC1155FacetHarness(); + erc1155Token.initialize(DEFAULT_URI); + + /** + * Deploy and initialize the staking harness + */ + facet = new StakingFacetHarness(); + + /** + * Initialize staking parameters + */ + facet.initialize( + BASE_APR, + REWARD_DECAY_RATE, + COMPOUND_FREQUENCY, + address(rewardToken), + COOLDOWN_PERIOD, + MIN_STAKE_AMOUNT, + MAX_STAKE_AMOUNT + ); + + /** + * Register supported tokens + */ + facet.addSupportedToken(address(erc20Token), true, false, false); + facet.addSupportedToken(address(erc721Token), false, true, false); + facet.addSupportedToken(address(erc1155Token), false, false, true); + + /** + * Mint tokens to Alice and Bob for testing + */ + erc20Token.mint(alice, 1000 ether); + erc20Token.mint(bob, 1000 ether); + erc721Token.mint(alice, TOKEN_ID_1); + erc721Token.mint(bob, TOKEN_ID_2); + erc1155Token.mint(alice, TOKEN_ID_1, 10); + erc1155Token.mint(bob, TOKEN_ID_2, 10); + rewardToken.mint(address(facet), 1_000_000 ether); + + vm.stopPrank(); + } + + function test_ParametersAreSetCorrectly() public { + ( + uint256 baseAPR, + uint256 rewardDecayRate, + uint256 compoundFrequency, + address rewardTokenAddress, + uint256 cooldownPeriod, + uint256 minStakeAmount, + uint256 maxStakeAmount + ) = facet.getStakingParameters(); + + assertEq(baseAPR, BASE_APR); + assertEq(rewardDecayRate, REWARD_DECAY_RATE); + assertEq(compoundFrequency, COMPOUND_FREQUENCY); + assertEq(rewardTokenAddress, address(rewardToken)); + assertEq(cooldownPeriod, COOLDOWN_PERIOD); + assertEq(minStakeAmount, MIN_STAKE_AMOUNT); + assertEq(maxStakeAmount, MAX_STAKE_AMOUNT); + } + + function test_StakeERC20Token() public { + vm.startPrank(alice); + + // Approve the staking contract to spend Alice's tokens + erc20Token.approve(address(facet), 500 ether); + + // Stake tokens + facet.stakeToken(address(erc20Token), 0, 500 ether); + + // Verify staking state + (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) = + facet.getStakedTokenInfo( + address(erc20Token), + 0 // Token ID is 0 for ERC-20 staking + ); + + assertEq(amount, 500 ether); + assertGt(stakedAt, 0); + assertGt(lastClaimedAt, 0); + assertEq(accumulatedRewards, 0); + + vm.stopPrank(); + } + + function test_StakeERC721Token() public { + vm.startPrank(bob); + + // Approve the staking contract to transfer Bob's NFT + erc721Token.approve(address(facet), TOKEN_ID_2); + + // Stake NFT + facet.stakeToken(address(erc721Token), TOKEN_ID_2, 1); + + // Verify staking state + (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) = + facet.getStakedTokenInfo(address(erc721Token), TOKEN_ID_2); + + assertEq(amount, 1); + assertGt(stakedAt, 0); + assertGt(lastClaimedAt, 0); + assertEq(accumulatedRewards, 0); + + vm.stopPrank(); + } + + function test_StakeERC1155Token() public { + vm.startPrank(alice); + + // Approve the staking contract to transfer Alice's ERC-1155 tokens + erc1155Token.setApprovalForAll(address(facet), true); + + // Stake ERC-1155 tokens + facet.stakeToken(address(erc1155Token), TOKEN_ID_1, 5); + + // Verify staking state + (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) = + facet.getStakedTokenInfo(address(erc1155Token), TOKEN_ID_1); + + assertEq(amount, 5); + assertGt(stakedAt, 0); + assertGt(lastClaimedAt, 0); + assertEq(accumulatedRewards, 0); + + vm.stopPrank(); + } + + function test_StakeToken_RevertsForUnsupportedToken() public { + address unsupportedToken = makeAddr("unsupportedToken"); + + vm.startPrank(alice); + + vm.expectRevert(abi.encodeWithSelector(StakingFacet.StakingUnsupportedToken.selector, unsupportedToken)); + facet.stakeToken(unsupportedToken, 0, 100 ether); + + vm.stopPrank(); + } + + function test_RewardCalculation() public { + vm.startPrank(alice); + + erc20Token.approve(address(facet), 1000 ether); + facet.stakeToken(address(erc20Token), 0, 1000 ether); + + vm.warp(block.timestamp + 365 days); + + uint256 calculatedRewards = facet.calculateRewardsForToken(address(erc20Token), 0); + + assertGt(calculatedRewards, 0); // 1000 ether * 10% * 1 year = 100 ether + vm.stopPrank(); + } + + function test_ClaimedRewards() public { + vm.startPrank(alice); + + erc20Token.approve(address(facet), 1000 ether); + facet.stakeToken(address(erc20Token), 0, 1000 ether); + + vm.warp(block.timestamp + 365 days); + + uint256 rewardsBalanceBefore = rewardToken.balanceOf(alice); + facet.claimRewards(address(erc20Token), 0); + uint256 rewardsBalanceAfter = rewardToken.balanceOf(alice); + + assertGt(rewardsBalanceAfter, rewardsBalanceBefore); // Alice should have received rewards + vm.stopPrank(); + } + + function test_Unstake() public { + vm.startPrank(alice); + + erc20Token.approve(address(facet), 1000 ether); + facet.stakeToken(address(erc20Token), 0, 1000 ether); + + vm.warp(block.timestamp + COOLDOWN_PERIOD + 1); + + facet.unstakeToken(address(erc20Token), 0); + + (uint256 amount,,,) = facet.getStakedTokenInfo(address(erc20Token), 0); + + assertEq(amount, 0); // Alice should have unstaked all tokens + + vm.stopPrank(); + } + + function test_Unstake_RevertsBeforeCooldown() public { + vm.startPrank(alice); + + erc20Token.approve(address(facet), 1000 ether); + facet.stakeToken(address(erc20Token), 0, 1000 ether); + + vm.warp(block.timestamp + COOLDOWN_PERIOD - 1); + + (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) = + facet.getStakedTokenInfo(address(erc20Token), 0); + + vm.expectRevert( + abi.encodeWithSelector( + StakingFacet.StakingCooldownNotElapsed.selector, stakedAt, COOLDOWN_PERIOD, block.timestamp + ) + ); + facet.unstakeToken(address(erc20Token), 0); + + vm.stopPrank(); + } +} diff --git a/test/token/Staking/harnesses/StakingFacetHarness.sol b/test/token/Staking/harnesses/StakingFacetHarness.sol index 7bac8739..457a1971 100644 --- a/test/token/Staking/harnesses/StakingFacetHarness.sol +++ b/test/token/Staking/harnesses/StakingFacetHarness.sol @@ -8,4 +8,59 @@ import {StakingFacet} from "../../../../src/token/Staking/StakingFacet.sol"; * @notice Test harness for StakingFacet * @dev Adds helper functions to set up staking state for testing */ -contract StakingFacetHarness is StakingFacet {} +contract StakingFacetHarness is StakingFacet { + /** + * @notice Initialize the staking storage for testing + * @dev Only used for testing purposes - production diamonds should initialize in constructor + * @dev Set staking parameters + * @param _baseAPR The base annual percentage rate for staking rewards + * @param _rewardDecayRate The decay rate for staking rewards + * @param _compoundFrequency The frequency at which rewards are compounded + * @param _rewardToken The address of the reward ERC-20 token + * @param _cooldownPeriod The cooldown period for unstaking + * @param _minStakeAmount The minimum amount that can be staked + * @param _maxStakeAmount The maximum amount that can be staked + */ + function initialize( + uint256 _baseAPR, + uint256 _rewardDecayRate, + uint256 _compoundFrequency, + address _rewardToken, + uint256 _cooldownPeriod, + uint256 _minStakeAmount, + uint256 _maxStakeAmount + ) external { + StakingStorage storage s = getStorage(); + s.baseAPR = _baseAPR; + s.rewardDecayRate = _rewardDecayRate; + s.compoundFrequency = _compoundFrequency; + s.rewardToken = _rewardToken; + s.cooldownPeriod = _cooldownPeriod; + s.minStakeAmount = _minStakeAmount; + s.maxStakeAmount = _maxStakeAmount; + } + + function stakeERC20Token(address _tokenAddress, uint256 _value) external { + stakeERC20(_tokenAddress, _value); + } + + function stakeERC721Token(address _tokenAddress, uint256 _tokenId) external { + stakeERC721(_tokenAddress, _tokenId); + } + + function stakeERC1155Token(address _tokenAddress, uint256 _tokenId, uint256 _value) external { + stakeERC1155(_tokenAddress, _tokenId, _value); + } + + function calculateRewardsForToken(address _tokenAddress, uint256 _tokenId) external view returns (uint256) { + return calculateRewards(_tokenAddress, _tokenId); + } + + function claimRewardsForToken(address _tokenAddress, uint256 _tokenId) external { + _claimRewards(_tokenAddress, _tokenId); + } + + function tokenSupported(address _tokenAddress) external view { + isTokenSupported(_tokenAddress); + } +} From fa6623257fa136eab26bf45c439b962f18c1d80d Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Sat, 20 Dec 2025 22:31:40 +0800 Subject: [PATCH 11/20] fixed overflow calculateReward for StakingFacet --- src/token/Staking/StakingFacet.sol | 68 ++++++++++++++++++- test/token/Staking/StakingFacet.t.sol | 42 ++++++++++++ .../Staking/harnesses/StakingFacetHarness.sol | 21 ++++++ 3 files changed, 129 insertions(+), 2 deletions(-) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index 8ca50d16..5d011b0a 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -184,6 +184,11 @@ contract StakingFacet { */ error StakingInsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed); + /** + * @notice Thrown when an overflow occurs during arithmetic operations. + */ + error StakingOverflow(); + /** * @notice Emitted when staking parameters are updated. * @param baseAPR The base annual percentage rate for rewards. @@ -618,8 +623,22 @@ contract StakingFacet { uint256 baseReward = (stake.amount * s.baseAPR * stakedDuration) / (365 days * 100); // Apply decay factor based on staking duration and compound frequency - uint256 decayFactor = - s.rewardDecayRate > 0 ? (s.rewardDecayRate ** (stakedDuration / s.compoundFrequency)) : 10 ** 18; + uint256 decayFactor; + if (s.rewardDecayRate > 0 && s.compoundFrequency > 0) { + uint256 exponent = stakedDuration / s.compoundFrequency; + + /** + * Cap exponent to prevent overflow + * With max exponent of 125, even 2e18^125 is manageable with rpow + */ + if (exponent > 125) { + exponent = 125; + } + + decayFactor = rpow(s.rewardDecayRate, exponent); + } else { + decayFactor = 1e18; + } uint256 finalReward = (baseReward * decayFactor) / (10 ** 18); @@ -660,6 +679,51 @@ contract StakingFacet { return tokenType.isERC20 || tokenType.isERC721 || tokenType.isERC1155; } + /** + * @notice Raises a 1e18 fixed-point number to an integer power, with 1e18 precision. + * @dev Implements binary exponentiation. Handles underflow and overflow safely. + * @param _base 1e18-scaled fixed-point base (e.g. 0.99e18, 1e18, 1.01e18). + * @param _exp Integer exponent (e.g staked duration / compound frequency). + * @return result Fixed-point result of base^exp, scaled by 1e18. + */ + function rpow(uint256 _base, uint256 _exp) internal pure returns (uint256 result) { + result = 1e18; // Initialize result as 1 in 1e18 fixed-point + uint256 base = _base; + + while (_exp > 0) { + if (_exp % 2 == 1) { + result = rmul(result, base); + } + base = rmul(base, base); + _exp /= 2; + } + + return result; + } + + /** + * @notice Multiplies two 1e18 fixed-point numbers, returning a 1e18 fixed-point result. + * @dev Equivalent to (x * y) / 1e18, rounded down. + */ + function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + if (x == 0 || y == 0) { + return 0; + } + + /** + * Check for overflow in multiplication + */ + if (x > type(uint256).max / y) { + revert StakingOverflow(); + } + + unchecked { + z = (x * y) / 1e18; + } + + return z; + } + /** * @notice Support Interface to satisfy ERC-165 standard. * @param interfaceId The interface identifier, as specified in ERC-165. diff --git a/test/token/Staking/StakingFacet.t.sol b/test/token/Staking/StakingFacet.t.sol index e857d022..2a7e8cfe 100644 --- a/test/token/Staking/StakingFacet.t.sol +++ b/test/token/Staking/StakingFacet.t.sol @@ -238,6 +238,48 @@ contract StakingFacetTest is Test { vm.stopPrank(); } + function test_FuzzRewardDecayMath(uint256 stakedSeconds, uint256 decayRate, uint256 compoundFreq) public { + vm.startPrank(owner); + // Clamp fuzzed values to safe bounds + stakedSeconds = bound(stakedSeconds, 1 days, 3650 days); // 1 day to 10 years + decayRate = bound(decayRate, 0.5e18, 1.5e18); // 50% to 150% + compoundFreq = bound(compoundFreq, 1 days, 365 days); // 1 day to 1 year + + facet.addSupportedToken(address(erc20Token), true, false, false); + facet.addSupportedToken(address(rewardToken), true, false, false); + + // set the staking parameters + facet.setStakingParameters( + 1000, decayRate, compoundFreq, address(rewardToken), COOLDOWN_PERIOD, 1, type(uint256).max + ); + + vm.stopPrank(); + + /** + * Warp time forward to ensure block.timestamp > stakedSeconds + * Add buffer to ensure we're well past the stake time + */ + vm.warp(stakedSeconds + 1 days); + + uint256 stakeTime = block.timestamp - stakedSeconds; + facet.testSetStakeInfo(alice, address(erc20Token), 0, 100 ether, stakeTime, stakeTime); + + vm.prank(alice); + uint256 rewards = facet.calculateRewardsForToken(address(erc20Token), 0); + + assertLe(rewards, type(uint256).max); + if (decayRate >= 1e18 && stakedSeconds < 10 * 365 days) { + assertGt(rewards, 0); + } + + if (rewards == 0 || rewards > 1_000_000 ether) { + emit log_named_uint("Decay Rate", decayRate); + emit log_named_uint("Compound Frequency", compoundFreq); + emit log_named_uint("Staked Seconds", stakedSeconds); + emit log_named_uint("Calculated Rewards", rewards); + } + } + function test_Unstake() public { vm.startPrank(alice); diff --git a/test/token/Staking/harnesses/StakingFacetHarness.sol b/test/token/Staking/harnesses/StakingFacetHarness.sol index 457a1971..8f75a925 100644 --- a/test/token/Staking/harnesses/StakingFacetHarness.sol +++ b/test/token/Staking/harnesses/StakingFacetHarness.sol @@ -63,4 +63,25 @@ contract StakingFacetHarness is StakingFacet { function tokenSupported(address _tokenAddress) external view { isTokenSupported(_tokenAddress); } + + function testSetStakeInfo( + address user, + address token, + uint256 tokenId, + uint256 amount, + uint256 stakedAt, + uint256 lastClaimedAt + ) external { + StakingStorage storage s = getStorage(); + StakedTokenInfo storage stake = s.stakedTokens[user][token][tokenId]; + + stake.amount = amount; + stake.stakedAt = stakedAt; + stake.lastClaimedAt = lastClaimedAt; + } + + function getStakeInfo(address user, address token, uint256 tokenId) external view returns (StakedTokenInfo memory) { + StakingStorage storage s = getStorage(); + return s.stakedTokens[user][token][tokenId]; + } } From fbb6246084b659e4c3d5a0944c3fa592df5fcf06 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Sun, 21 Dec 2025 09:13:17 +0800 Subject: [PATCH 12/20] fixed unstakeToken for StakingFacet --- src/token/Staking/StakingFacet.sol | 6 +- test/token/Staking/StakingFacet.t.sol | 193 ++++++++++++++++++++------ 2 files changed, 159 insertions(+), 40 deletions(-) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index 5d011b0a..1fb4e4a5 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -328,12 +328,14 @@ contract StakingFacet { TokenType storage tokenType = s.supportedTokens[_tokenAddress]; bool isSupported = isTokenSupported(_tokenAddress); + bool isTokenERC20 = s.supportedTokens[_tokenAddress].isERC20; + if (!isSupported) { revert StakingUnsupportedToken(_tokenAddress); } if (s.minStakeAmount > 0) { - if (_amount <= s.minStakeAmount) { + if (isTokenERC20 && _amount <= s.minStakeAmount) { revert StakingAmountBelowMinimum(_amount, s.minStakeAmount); } } @@ -605,6 +607,8 @@ contract StakingFacet { /** * @notice Calculates the rewards for a staked token. * @dev Uses base APR, decay rate, and compounding frequency to compute rewards. + * @dev Rewards are calculated based on the time since last claim. + * @dev Uses fixed-point arithmetic with 1e18 precision. * @param _tokenAddress The address of the staked token. * @param _tokenId The ID of the staked token. * @return finalReward The calculated reward amount. diff --git a/test/token/Staking/StakingFacet.t.sol b/test/token/Staking/StakingFacet.t.sol index 2a7e8cfe..3b9c4980 100644 --- a/test/token/Staking/StakingFacet.t.sol +++ b/test/token/Staking/StakingFacet.t.sol @@ -38,9 +38,12 @@ contract StakingFacetTest is Test { uint256 constant REWARD_DECAY_RATE = 0; // no decay uint256 constant COMPOUND_FREQUENCY = 365 days; uint256 constant COOLDOWN_PERIOD = 1 days; - uint256 constant MIN_STAKE_AMOUNT = 0 ether; + uint256 constant MIN_STAKE_AMOUNT = 1 ether; uint256 constant MAX_STAKE_AMOUNT = 1_000_000 ether; + event TokensStaked(address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 amount); + event TokensUnstaked(address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 amount); + function setUp() public { alice = makeAddr("alice"); bob = makeAddr("bob"); @@ -197,6 +200,22 @@ contract StakingFacetTest is Test { vm.stopPrank(); } + function test_StakeToken_EmitTokensStakedEvent() public { + vm.startPrank(alice); + + // Approve the staking contract to spend Alice's tokens + erc20Token.approve(address(facet), 500 ether); + + // Expect event + vm.expectEmit(true, true, true, true); + emit TokensStaked(alice, address(erc20Token), 0, 500 ether); + + // Stake tokens + facet.stakeToken(address(erc20Token), 0, 500 ether); + + vm.stopPrank(); + } + function test_StakeToken_RevertsForUnsupportedToken() public { address unsupportedToken = makeAddr("unsupportedToken"); @@ -208,6 +227,140 @@ contract StakingFacetTest is Test { vm.stopPrank(); } + function test_StakeToken_RevertsForMinimumAmount() public { + vm.startPrank(alice); + + vm.expectRevert( + abi.encodeWithSelector(StakingFacet.StakingAmountBelowMinimum.selector, 0 ether, MIN_STAKE_AMOUNT) + ); + facet.stakeToken(address(erc20Token), 0, 0 ether); + + vm.stopPrank(); + } + + function test_StakeToken_RevertsForMaximumAmount() public { + vm.startPrank(alice); + + vm.expectRevert( + abi.encodeWithSelector(StakingFacet.StakingAmountAboveMaximum.selector, 2_000_000 ether, MAX_STAKE_AMOUNT) + ); + facet.stakeToken(address(erc20Token), 0, 2_000_000 ether); + + vm.stopPrank(); + } + + function test_StakeERC721Token_RevertsIfNotOwner() public { + vm.startPrank(bob); + + // Bob tries to stake Alice's NFT + vm.expectRevert( + abi.encodeWithSelector(StakingFacet.StakingNotTokenOwner.selector, bob, address(erc721Token), TOKEN_ID_1) + ); + facet.stakeToken(address(erc721Token), TOKEN_ID_1, 1); + + vm.stopPrank(); + } + + function test_StakeERC1155Token_RevertsIfNotEnoughBalance() public { + vm.startPrank(bob); + + // Bob tries to stake more ERC-1155 tokens than he owns + vm.expectRevert(abi.encodeWithSelector(StakingFacet.StakingInsufficientBalance.selector, bob, 10, 20)); + facet.stakeToken(address(erc1155Token), TOKEN_ID_2, 20); + + vm.stopPrank(); + } + + function test_UnstakeERC20Token() public { + vm.startPrank(alice); + + // Approve and stake tokens + erc20Token.approve(address(facet), 500 ether); + facet.stakeToken(address(erc20Token), 0, 500 ether); + + // Warp time to pass cooldown period + vm.warp(block.timestamp + COOLDOWN_PERIOD + 1); + + // Unstake tokens + facet.unstakeToken(address(erc20Token), 0); + + // Verify staking state is reset + (uint256 amount,,,) = facet.getStakedTokenInfo(address(erc20Token), 0); + assertEq(amount, 0); + + vm.stopPrank(); + } + + function test_UnstakeERC721Token() public { + vm.startPrank(bob); + + // Approve and stake NFT + erc721Token.approve(address(facet), TOKEN_ID_2); + facet.stakeToken(address(erc721Token), TOKEN_ID_2, 1); + + // Warp time to pass cooldown period + vm.warp(block.timestamp + COOLDOWN_PERIOD + 1); + + // Unstake NFT + facet.unstakeToken(address(erc721Token), TOKEN_ID_2); + + // Verify staking state is reset + (uint256 amount,,,) = facet.getStakedTokenInfo(address(erc721Token), TOKEN_ID_2); + assertEq(amount, 0); + + vm.stopPrank(); + } + + function test_UnstakeERC1155Token() public { + vm.startPrank(alice); + + // Approve and stake ERC-1155 tokens + erc1155Token.setApprovalForAll(address(facet), true); + facet.stakeToken(address(erc1155Token), TOKEN_ID_1, 5); + + // Warp time to pass cooldown period + vm.warp(block.timestamp + COOLDOWN_PERIOD + 1); + + // Unstake ERC-1155 tokens + facet.unstakeToken(address(erc1155Token), TOKEN_ID_1); + + // Verify staking state is reset + (uint256 amount,,,) = facet.getStakedTokenInfo(address(erc1155Token), TOKEN_ID_1); + assertEq(amount, 0); + + vm.stopPrank(); + } + + function test_Unstake_RevertsBeforeCooldown() public { + vm.startPrank(alice); + + erc20Token.approve(address(facet), 1000 ether); + facet.stakeToken(address(erc20Token), 0, 1000 ether); + + vm.warp(block.timestamp + COOLDOWN_PERIOD - 1); + + (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) = + facet.getStakedTokenInfo(address(erc20Token), 0); + + vm.expectRevert( + abi.encodeWithSelector( + StakingFacet.StakingCooldownNotElapsed.selector, stakedAt, COOLDOWN_PERIOD, block.timestamp + ) + ); + facet.unstakeToken(address(erc20Token), 0); + + vm.stopPrank(); + } + + function test_Unstake_RevertsZeroStakeAmount() public { + vm.startPrank(alice); + + vm.expectRevert(abi.encodeWithSelector(StakingFacet.StakingZeroStakeAmount.selector)); + facet.unstakeToken(address(erc20Token), 0); + + vm.stopPrank(); + } + function test_RewardCalculation() public { vm.startPrank(alice); @@ -279,42 +432,4 @@ contract StakingFacetTest is Test { emit log_named_uint("Calculated Rewards", rewards); } } - - function test_Unstake() public { - vm.startPrank(alice); - - erc20Token.approve(address(facet), 1000 ether); - facet.stakeToken(address(erc20Token), 0, 1000 ether); - - vm.warp(block.timestamp + COOLDOWN_PERIOD + 1); - - facet.unstakeToken(address(erc20Token), 0); - - (uint256 amount,,,) = facet.getStakedTokenInfo(address(erc20Token), 0); - - assertEq(amount, 0); // Alice should have unstaked all tokens - - vm.stopPrank(); - } - - function test_Unstake_RevertsBeforeCooldown() public { - vm.startPrank(alice); - - erc20Token.approve(address(facet), 1000 ether); - facet.stakeToken(address(erc20Token), 0, 1000 ether); - - vm.warp(block.timestamp + COOLDOWN_PERIOD - 1); - - (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) = - facet.getStakedTokenInfo(address(erc20Token), 0); - - vm.expectRevert( - abi.encodeWithSelector( - StakingFacet.StakingCooldownNotElapsed.selector, stakedAt, COOLDOWN_PERIOD, block.timestamp - ) - ); - facet.unstakeToken(address(erc20Token), 0); - - vm.stopPrank(); - } } From b4f0a0e1235ecd0a16cbf50d23dd4fced9995a80 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Sun, 21 Dec 2025 20:13:52 +0800 Subject: [PATCH 13/20] added internal math for StakingMod --- src/token/Staking/StakingFacet.sol | 15 ++- src/token/Staking/StakingMod.sol | 50 ++++++++++ test/token/Staking/Staking.t.sol | 28 ++++-- test/token/Staking/StakingFacet.t.sol | 96 ++++++++++++++++++- .../Staking/harnesses/StakingFacetHarness.sol | 9 +- 5 files changed, 185 insertions(+), 13 deletions(-) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index 1fb4e4a5..36e7a999 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -126,6 +126,11 @@ contract StakingFacet { */ error StakingAmountBelowMinimum(uint256 amount, uint256 minAmount); + /** + * @notice Thrown when a token transfer fails. + */ + error StakingTransferFailed(); + /** * @notice Thrown when attempting to stake an amount above the maximum stake amount. * @param amount The attempted stake amount. @@ -346,7 +351,10 @@ contract StakingFacet { } if (s.supportedTokens[_tokenAddress].isERC20) { - IERC20(_tokenAddress).transferFrom(msg.sender, address(this), _amount); + bool success = IERC20(_tokenAddress).transferFrom(msg.sender, address(this), _amount); + if (!success) { + revert StakingTransferFailed(); + } stakeERC20(_tokenAddress, _amount); } else if (s.supportedTokens[_tokenAddress].isERC721) { if (IERC721(_tokenAddress).ownerOf(_tokenId) != msg.sender) { @@ -664,7 +672,10 @@ contract StakingFacet { revert StakingNoRewardsToClaim(_tokenAddress, _tokenId); } - IERC20(s.rewardToken).transfer(msg.sender, rewards); + bool success = IERC20(s.rewardToken).transfer(msg.sender, rewards); + if (!success) { + revert StakingTransferFailed(); + } stake.lastClaimedAt = block.timestamp; stake.accumulatedRewards += rewards; diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol index 1905fb91..64ddd125 100644 --- a/src/token/Staking/StakingMod.sol +++ b/src/token/Staking/StakingMod.sol @@ -19,6 +19,11 @@ error StakingUnsupportedToken(address tokenAddress); */ error StakingZeroStakeAmount(); +/** + * @notice Thrown when an overflow occurs during arithmetic operations. + */ +error StakingOverflow(); + /** * @notice Emitted when staking parameters are updated. * @param baseAPR The base annual percentage rate for rewards. @@ -296,3 +301,48 @@ function isTokenSupported(address _tokenAddress) view returns (bool) { TokenType storage tokenType = s.supportedTokens[_tokenAddress]; return tokenType.isERC20 || tokenType.isERC721 || tokenType.isERC1155; } + +/** + * @notice Raises a 1e18 fixed-point number to an integer power, with 1e18 precision. + * @dev Implements binary exponentiation. Handles underflow and overflow safely. + * @param _base 1e18-scaled fixed-point base (e.g. 0.99e18, 1e18, 1.01e18). + * @param _exp Integer exponent (e.g staked duration / compound frequency). + * @return result Fixed-point result of base^exp, scaled by 1e18. + */ +function rpow(uint256 _base, uint256 _exp) pure returns (uint256 result) { + result = 1e18; // Initialize result as 1 in 1e18 fixed-point + uint256 base = _base; + + while (_exp > 0) { + if (_exp % 2 == 1) { + result = rmul(result, base); + } + base = rmul(base, base); + _exp /= 2; + } + + return result; +} + +/** + * @notice Multiplies two 1e18 fixed-point numbers, returning a 1e18 fixed-point result. + * @dev Equivalent to (x * y) / 1e18, rounded down. + */ +function rmul(uint256 x, uint256 y) pure returns (uint256 z) { + if (x == 0 || y == 0) { + return 0; + } + + /** + * Check for overflow in multiplication + */ + if (x > type(uint256).max / y) { + revert StakingOverflow(); + } + + unchecked { + z = (x * y) / 1e18; + } + + return z; +} diff --git a/test/token/Staking/Staking.t.sol b/test/token/Staking/Staking.t.sol index d8080381..1d2bb90c 100644 --- a/test/token/Staking/Staking.t.sol +++ b/test/token/Staking/Staking.t.sol @@ -6,7 +6,7 @@ import {LibStakingHarness} from "./harnesses/LibStakingHarness.sol"; import {ERC20FacetHarness} from "../ERC20//ERC20/harnesses/ERC20FacetHarness.sol"; import {ERC721FacetHarness} from "../ERC721/ERC721/harnesses/ERC721FacetHarness.sol"; import {ERC1155FacetHarness} from "../ERC1155/ERC1155/harnesses/ERC1155FacetHarness.sol"; -import "../../../../src/token/Staking/StakingMod.sol" as StakingMod; +import "../../../src/token/Staking/StakingMod.sol" as StakingMod; // import "forge-std/console.sol"; @@ -93,6 +93,17 @@ contract StakingTest is Test { staking.addSupportedToken(address(erc1155Token), false, false, true); staking.addSupportedToken(address(rewardToken), true, false, false); + /** + * Mint tokens to Alice and Bob for testing + */ + erc20Token.mint(alice, 1_000 ether); + erc20Token.mint(bob, 1_000 ether); + erc1155Token.mint(bob, TOKEN_ID_2, 100); + erc1155Token.mint(alice, TOKEN_ID_1, 100); + erc721Token.mint(bob, TOKEN_ID_2); + erc721Token.mint(alice, TOKEN_ID_1); + rewardToken.mint(address(staking), 10_000 ether); + /** * Initialize staking parameters */ @@ -229,6 +240,9 @@ contract StakingTest is Test { function test_StakeERC20UpdatesState() public { uint256 stakeAmount = 100 ether; + vm.startPrank(alice); + + erc20Token.approve(address(staking), stakeAmount); // Stake ERC-20 tokens staking.stakeERC20(address(erc20Token), stakeAmount); @@ -236,36 +250,36 @@ contract StakingTest is Test { (uint256 amount,,,) = staking.getStakedTokenInfo(address(erc20Token), 0); assertEq(amount, stakeAmount); + vm.stopPrank(); } function test_StakeERC721UpdatesState() public { - // Mint an ERC-721 token to Alice - erc721Token.mint(alice, TOKEN_ID_1); + vm.startPrank(alice); // Stake ERC-721 token - vm.prank(alice); staking.stakeERC721(address(erc721Token), TOKEN_ID_1); (uint256 amount,,,) = staking.getStakedTokenInfo(address(erc721Token), TOKEN_ID_1); console.log("Staked amount:", amount); assertEq(amount, 1); + vm.stopPrank(); } function test_StakeERC1155UpdatesState() public { uint256 stakeAmount = 10; + vm.startPrank(bob); - // Mint ERC-1155 tokens to Bob - erc1155Token.mint(bob, TOKEN_ID_2, stakeAmount); + erc1155Token.setApprovalForAll(address(staking), true); // Stake ERC-1155 tokens - vm.prank(bob); staking.stakeERC1155(address(erc1155Token), TOKEN_ID_2, stakeAmount); (uint256 amount,,,) = staking.getStakedTokenInfo(address(erc1155Token), TOKEN_ID_2); console.log("Staked amount:", amount); assertEq(amount, stakeAmount); + vm.stopPrank(); } function test_StakeTokens_RevertWithUnsupportedToken() public { diff --git a/test/token/Staking/StakingFacet.t.sol b/test/token/Staking/StakingFacet.t.sol index 3b9c4980..972747ff 100644 --- a/test/token/Staking/StakingFacet.t.sol +++ b/test/token/Staking/StakingFacet.t.sol @@ -43,6 +43,15 @@ contract StakingFacetTest is Test { event TokensStaked(address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 amount); event TokensUnstaked(address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 amount); + event StakingParametersUpdated( + uint256 baseAPR, + uint256 rewardDecayRate, + uint256 compoundFrequency, + address rewardToken, + uint256 cooldownPeriod, + uint256 minStakeAmount, + uint256 maxStakeAmount + ); function setUp() public { alice = makeAddr("alice"); @@ -134,6 +143,37 @@ contract StakingFacetTest is Test { assertEq(maxStakeAmount, MAX_STAKE_AMOUNT); } + function test_ParametersAreSetCorrectly_EmitEventStakingParametersUpdated() public { + vm.startPrank(owner); + + facet.addSupportedToken(address(rewardToken), true, false, false); + + // Expect event + vm.expectEmit(true, true, true, true); + emit StakingParametersUpdated( + BASE_APR, + REWARD_DECAY_RATE, + COMPOUND_FREQUENCY, + address(rewardToken), + COOLDOWN_PERIOD, + MIN_STAKE_AMOUNT, + MAX_STAKE_AMOUNT + ); + + // Re-initialize to trigger event + facet.setStakingParameters( + BASE_APR, + REWARD_DECAY_RATE, + COMPOUND_FREQUENCY, + address(rewardToken), + COOLDOWN_PERIOD, + MIN_STAKE_AMOUNT, + MAX_STAKE_AMOUNT + ); + + vm.stopPrank(); + } + function test_StakeERC20Token() public { vm.startPrank(alice); @@ -331,6 +371,26 @@ contract StakingFacetTest is Test { vm.stopPrank(); } + function test_UnstakeToken_EmitTokensUnstakedEvent() public { + vm.startPrank(alice); + + // Approve and stake tokens + erc20Token.approve(address(facet), 500 ether); + facet.stakeToken(address(erc20Token), 0, 500 ether); + + // Warp time to pass cooldown period + vm.warp(block.timestamp + COOLDOWN_PERIOD + 1); + + // Expect event + vm.expectEmit(true, true, true, true); + emit TokensUnstaked(alice, address(erc20Token), 0, 500 ether); + + // Unstake tokens + facet.unstakeToken(address(erc20Token), 0); + + vm.stopPrank(); + } + function test_Unstake_RevertsBeforeCooldown() public { vm.startPrank(alice); @@ -387,11 +447,17 @@ contract StakingFacetTest is Test { facet.claimRewards(address(erc20Token), 0); uint256 rewardsBalanceAfter = rewardToken.balanceOf(alice); + (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) = + facet.getStakedTokenInfo(address(erc20Token), 0); + assertGt(rewardsBalanceAfter, rewardsBalanceBefore); // Alice should have received rewards + assertEq(accumulatedRewards, rewardsBalanceAfter - rewardsBalanceBefore); + assertEq(lastClaimedAt, block.timestamp); + assertEq(amount, 1000 ether); // Staked amount should remain unchanged vm.stopPrank(); } - function test_FuzzRewardDecayMath(uint256 stakedSeconds, uint256 decayRate, uint256 compoundFreq) public { + function testFuzz_RewardDecayMath(uint256 stakedSeconds, uint256 decayRate, uint256 compoundFreq) public { vm.startPrank(owner); // Clamp fuzzed values to safe bounds stakedSeconds = bound(stakedSeconds, 1 days, 3650 days); // 1 day to 10 years @@ -432,4 +498,32 @@ contract StakingFacetTest is Test { emit log_named_uint("Calculated Rewards", rewards); } } + + function test_FixedPoint_IntegerPower() public { + uint256 result = facet.rPow(2e18, 3); // 2^3 = 8 + assertEq(result, 8e18); + + result = facet.rPow(5e18, 0); // 5^0 = 1 + assertEq(result, 1e18); + + result = facet.rPow(1e18, 10); // 1^10 = 1 + assertEq(result, 1e18); + + result = facet.rPow(3e18, 4); // 3^4 = 81 + assertEq(result, 81e18); + } + + function test_FixedPoint_Multiply() public { + uint256 result = facet.rMul(2e18, 3e18); // 2 * 3 = 6 + assertEq(result, 6e18); + + result = facet.rMul(5e18, 0e18); // 5 * 0 = 0 + assertEq(result, 0); + + result = facet.rMul(1e18, 10e18); // 1 * 10 = 10 + assertEq(result, 10e18); + + result = facet.rMul(3e18, 4e18); // 3 * 4 = 12 + assertEq(result, 12e18); + } } diff --git a/test/token/Staking/harnesses/StakingFacetHarness.sol b/test/token/Staking/harnesses/StakingFacetHarness.sol index 8f75a925..4d0b08aa 100644 --- a/test/token/Staking/harnesses/StakingFacetHarness.sol +++ b/test/token/Staking/harnesses/StakingFacetHarness.sol @@ -80,8 +80,11 @@ contract StakingFacetHarness is StakingFacet { stake.lastClaimedAt = lastClaimedAt; } - function getStakeInfo(address user, address token, uint256 tokenId) external view returns (StakedTokenInfo memory) { - StakingStorage storage s = getStorage(); - return s.stakedTokens[user][token][tokenId]; + function rPow(uint256 _base, uint256 _exp) external pure returns (uint256) { + return rpow(_base, _exp); + } + + function rMul(uint256 _a, uint256 _b) external pure returns (uint256) { + return rmul(_a, _b); } } From 313dcb55666380fd24a3c22c35cb7fd55c5f9914 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Mon, 22 Dec 2025 07:20:33 +0800 Subject: [PATCH 14/20] fuzz test with fixes --- src/token/Staking/StakingFacet.sol | 2 +- test/token/Staking/Staking.t.sol | 28 +++++++ test/token/Staking/StakingFacet.t.sol | 83 ++++++++++++++++++- .../Staking/harnesses/LibStakingHarness.sol | 14 ++++ 4 files changed, 125 insertions(+), 2 deletions(-) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index 36e7a999..7694c0fd 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -340,7 +340,7 @@ contract StakingFacet { } if (s.minStakeAmount > 0) { - if (isTokenERC20 && _amount <= s.minStakeAmount) { + if (isTokenERC20 && _amount < s.minStakeAmount) { revert StakingAmountBelowMinimum(_amount, s.minStakeAmount); } } diff --git a/test/token/Staking/Staking.t.sol b/test/token/Staking/Staking.t.sol index 1d2bb90c..634fcd9f 100644 --- a/test/token/Staking/Staking.t.sol +++ b/test/token/Staking/Staking.t.sol @@ -288,4 +288,32 @@ contract StakingTest is Test { vm.expectRevert(abi.encodeWithSelector(StakingMod.StakingUnsupportedToken.selector, unsupportedToken)); staking.stakeERC20(unsupportedToken, 100 ether); } + + function test_FixedPoint_IntegerPower() public { + uint256 result = staking.rPow(2e18, 3); // 2^3 = 8 + assertEq(result, 8e18); + + result = staking.rPow(5e18, 0); // 5^0 = 1 + assertEq(result, 1e18); + + result = staking.rPow(1e18, 10); // 1^10 = 1 + assertEq(result, 1e18); + + result = staking.rPow(3e18, 4); // 3^4 = 81 + assertEq(result, 81e18); + } + + function test_FixedPoint_Multiply() public { + uint256 result = staking.rMul(2e18, 3e18); // 2 * 3 = 6 + assertEq(result, 6e18); + + result = staking.rMul(5e18, 0e18); // 5 * 0 = 0 + assertEq(result, 0); + + result = staking.rMul(1e18, 10e18); // 1 * 10 = 10 + assertEq(result, 10e18); + + result = staking.rMul(3e18, 4e18); // 3 * 4 = 12 + assertEq(result, 12e18); + } } diff --git a/test/token/Staking/StakingFacet.t.sol b/test/token/Staking/StakingFacet.t.sol index 972747ff..d1895f78 100644 --- a/test/token/Staking/StakingFacet.t.sol +++ b/test/token/Staking/StakingFacet.t.sol @@ -457,7 +457,54 @@ contract StakingFacetTest is Test { vm.stopPrank(); } - function testFuzz_RewardDecayMath(uint256 stakedSeconds, uint256 decayRate, uint256 compoundFreq) public { + // Fuzz Tests for ERC-20 Staking + function testFuzz_StakeAmount(uint256 stakeAmount) public { + stakeAmount = bound(stakeAmount, 1e18, 1_000_000e18); // Clamp between 1 and 1,000,000 tokens + + vm.startPrank(owner); + facet.addSupportedToken(address(erc20Token), true, false, false); + facet.addSupportedToken(address(rewardToken), true, false, false); + facet.setStakingParameters(1000, 1e18, 1 days, address(rewardToken), 0, 1e18, type(uint256).max); + vm.stopPrank(); + + erc20Token.mint(alice, stakeAmount); + rewardToken.mint(address(facet), 1_000_000 ether); + + vm.startPrank(alice); + erc20Token.approve(address(facet), stakeAmount); + facet.stakeToken(address(erc20Token), 0, stakeAmount); + + (uint256 amount,,,) = facet.getStakedTokenInfo(address(erc20Token), 0); + assertEq(amount, stakeAmount); + vm.stopPrank(); + } + + // Fuzz Test for long staking durations + function testFuzz_RewardAfterTime(uint256 daysStaked) public { + daysStaked = bound(daysStaked, 1, 3650); // 1 day to 10 years + + vm.startPrank(owner); + facet.addSupportedToken(address(erc20Token), true, false, false); + facet.addSupportedToken(address(rewardToken), true, false, false); + facet.setStakingParameters(1000, 1e18, 1 days, address(rewardToken), 0, 1e18, type(uint256).max); + vm.stopPrank(); + + erc20Token.mint(alice, 100 ether); + rewardToken.mint(address(facet), 1_000_000 ether); + + vm.startPrank(alice); + erc20Token.approve(address(facet), 100 ether); + facet.stakeToken(address(erc20Token), 0, 100 ether); + + vm.warp(block.timestamp + (daysStaked * 1 days)); + uint256 rewards = facet.calculateRewardsForToken(address(erc20Token), 0); + + assertGt(rewards, 0); + assertLe(rewards, 1_000_000 ether); // Should not exceed total reward pool + vm.stopPrank(); + } + + function testFuzz_RewardDecay(uint256 stakedSeconds, uint256 decayRate, uint256 compoundFreq) public { vm.startPrank(owner); // Clamp fuzzed values to safe bounds stakedSeconds = bound(stakedSeconds, 1 days, 3650 days); // 1 day to 10 years @@ -499,6 +546,40 @@ contract StakingFacetTest is Test { } } + function testFuzz_StakeUnstake(uint256 stakeAmount, uint256 waitDays) public { + stakeAmount = bound(stakeAmount, 1e18, 1_000e18); // 1 to 1,000 tokens + waitDays = bound(waitDays, 1, 365); // 1 to 365 days + + vm.startPrank(owner); + facet.addSupportedToken(address(erc20Token), true, false, false); + facet.addSupportedToken(address(rewardToken), true, false, false); + facet.setStakingParameters(1000, 1e18, 1 days, address(rewardToken), 1 days, 1e18, type(uint256).max); + vm.stopPrank(); + + erc20Token.mint(alice, stakeAmount); + rewardToken.mint(address(facet), 1_000_000 ether); + + vm.startPrank(alice); + erc20Token.approve(address(facet), stakeAmount); + facet.stakeToken(address(erc20Token), 0, stakeAmount); + + vm.warp(block.timestamp + (waitDays * 1 days) + COOLDOWN_PERIOD + 1); + + facet.unstakeToken(address(erc20Token), 0); + (uint256 amount,,,) = facet.getStakedTokenInfo(address(erc20Token), 0); + assertEq(amount, 0); + vm.stopPrank(); + } + + function testFuzz_UnsupportedToken(address randomToken) public { + vm.assume(randomToken != address(erc20Token) && randomToken != address(0)); + + vm.startPrank(alice); + vm.expectRevert(abi.encodeWithSelector(StakingFacet.StakingUnsupportedToken.selector, randomToken)); + facet.stakeToken(randomToken, 0, 100 ether); + vm.stopPrank(); + } + function test_FixedPoint_IntegerPower() public { uint256 result = facet.rPow(2e18, 3); // 2^3 = 8 assertEq(result, 8e18); diff --git a/test/token/Staking/harnesses/LibStakingHarness.sol b/test/token/Staking/harnesses/LibStakingHarness.sol index f58e9c78..ce5fd526 100644 --- a/test/token/Staking/harnesses/LibStakingHarness.sol +++ b/test/token/Staking/harnesses/LibStakingHarness.sol @@ -89,4 +89,18 @@ contract LibStakingHarness { function isTokenSupported(address _tokenAddress) external view returns (bool) { return StakingMod.isTokenSupported(_tokenAddress); } + + /** + * @notice Exposes StakingMod.rpow as an external function + */ + function rPow(uint256 _base, uint256 _exp) external pure returns (uint256) { + return StakingMod.rpow(_base, _exp); + } + + /** + * @notice Exposes StakingMod.rmul as an external function + */ + function rMul(uint256 _a, uint256 _b) external pure returns (uint256) { + return StakingMod.rmul(_a, _b); + } } From fd9eca4d8d3702f19d08013b3c3d1ca89d3cdaeb Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Mon, 22 Dec 2025 08:11:18 +0800 Subject: [PATCH 15/20] remove unnecessary events and more readable functions --- src/token/Staking/StakingFacet.sol | 165 ++++++++++++----------------- 1 file changed, 68 insertions(+), 97 deletions(-) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index 7694c0fd..a38fe962 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -169,18 +169,6 @@ contract StakingFacet { */ error StakingInsufficientBalance(address _sender, uint256 _balance, uint256 _needed); - /** - * @notice Thrown when the sender address is invalid (e.g., zero address). - * @param _sender Invalid sender address. - */ - error StakingInvalidSender(address _sender); - - /** - * @notice Thrown when the receiver address is invalid (e.g., zero address). - * @param _receiver Invalid receiver address. - */ - error StakingInvalidReceiver(address _receiver); - /** * @notice Thrown when a spender tries to use more than the approved allowance. * @param _spender Address attempting to spend. @@ -248,23 +236,6 @@ contract StakingFacet { address indexed staker, address indexed tokenAddress, uint256 indexed tokenId, uint256 rewardAmount ); - /** - * @notice Emitted when an approval is made for a spender by an owner. - * @param _owner The address granting the allowance. - * @param _spender The address receiving the allowance. - * @param _oldValue The previous allowance amount. - * @param _newValue The new allowance amount. - */ - event Approval(address indexed _owner, address indexed _spender, uint256 _oldValue, uint256 _newValue); - - /** - * @notice Emitted when tokens are transferred between two addresses. - * @param _from Address sending the tokens. - * @param _to Address receiving the tokens. - * @param _value Amount of tokens transferred. - */ - event Transfer(address indexed _from, address indexed _to, uint256 _value); - bytes32 constant STAKING_STORAGE_POSITION = keccak256("compose.staking"); /** @@ -322,6 +293,74 @@ contract StakingFacet { } } + /** + * @notice An admin function to support a new token type for staking. + * @param _tokenAddress The address of the token to support. + * @param _isERC20 Boolean indicating if the token is ERC-20. + * @param _isERC721 Boolean indicating if the token is ERC-721. + * @param _isERC1155 Boolean indicating if the token is ERC-1155 + * @dev This function should be restricted to admin use only. + */ + function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) + external + returns (bool) + { + StakingStorage storage s = getStorage(); + s.supportedTokens[_tokenAddress] = TokenType({isERC20: _isERC20, isERC721: _isERC721, isERC1155: _isERC1155}); + emit SupportedTokenAdded(_tokenAddress, _isERC20, _isERC721, _isERC1155); + return true; + } + + /** + * @notice An admin function to set staking parameters. + * @param _baseAPR The base annual percentage rate for rewards. + * @param _rewardDecayRate The decay rate for rewards over time. + * @param _compoundFrequency The frequency at which rewards are compounded. + * @param _rewardToken The address of the token used for rewards. + * @param _cooldownPeriod The cooldown period before unstaking is allowed. + * @param _minStakeAmount The minimum amount required to stake. + * @param _maxStakeAmount The maximum amount allowed to stake. + * @dev This function should be restricted to admin use only. + */ + function setStakingParameters( + uint256 _baseAPR, + uint256 _rewardDecayRate, + uint256 _compoundFrequency, + address _rewardToken, + uint256 _cooldownPeriod, + uint256 _minStakeAmount, + uint256 _maxStakeAmount + ) external { + StakingStorage storage s = getStorage(); + + bool isSupported = isTokenSupported(_rewardToken); + if (!isSupported) { + revert StakingUnsupportedToken(_rewardToken); + } + + if (_minStakeAmount == 0 || _maxStakeAmount == 0) { + revert StakingZeroStakeAmount(); + } + + s.baseAPR = _baseAPR; + s.rewardDecayRate = _rewardDecayRate; + s.compoundFrequency = _compoundFrequency; + s.rewardToken = _rewardToken; + s.cooldownPeriod = _cooldownPeriod; + s.minStakeAmount = _minStakeAmount; + s.maxStakeAmount = _maxStakeAmount; + + emit StakingParametersUpdated( + _baseAPR, + _rewardDecayRate, + _compoundFrequency, + _rewardToken, + _cooldownPeriod, + _minStakeAmount, + _maxStakeAmount + ); + } + /** * @notice Stakes tokens of a supported type. * @param _tokenAddress The address of the token to stake. @@ -413,74 +452,6 @@ contract StakingFacet { delete s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; } - /** - * @notice An admin function to support a new token type for staking. - * @param _tokenAddress The address of the token to support. - * @param _isERC20 Boolean indicating if the token is ERC-20. - * @param _isERC721 Boolean indicating if the token is ERC-721. - * @param _isERC1155 Boolean indicating if the token is ERC-1155 - * @dev This function should be restricted to admin use only. - */ - function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, bool _isERC1155) - external - returns (bool) - { - StakingStorage storage s = getStorage(); - s.supportedTokens[_tokenAddress] = TokenType({isERC20: _isERC20, isERC721: _isERC721, isERC1155: _isERC1155}); - emit SupportedTokenAdded(_tokenAddress, _isERC20, _isERC721, _isERC1155); - return true; - } - - /** - * @notice An admin function to set staking parameters. - * @param _baseAPR The base annual percentage rate for rewards. - * @param _rewardDecayRate The decay rate for rewards over time. - * @param _compoundFrequency The frequency at which rewards are compounded. - * @param _rewardToken The address of the token used for rewards. - * @param _cooldownPeriod The cooldown period before unstaking is allowed. - * @param _minStakeAmount The minimum amount required to stake. - * @param _maxStakeAmount The maximum amount allowed to stake. - * @dev This function should be restricted to admin use only. - */ - function setStakingParameters( - uint256 _baseAPR, - uint256 _rewardDecayRate, - uint256 _compoundFrequency, - address _rewardToken, - uint256 _cooldownPeriod, - uint256 _minStakeAmount, - uint256 _maxStakeAmount - ) external { - StakingStorage storage s = getStorage(); - - bool isSupported = isTokenSupported(_rewardToken); - if (!isSupported) { - revert StakingUnsupportedToken(_rewardToken); - } - - if (_minStakeAmount == 0 || _maxStakeAmount == 0) { - revert StakingZeroStakeAmount(); - } - - s.baseAPR = _baseAPR; - s.rewardDecayRate = _rewardDecayRate; - s.compoundFrequency = _compoundFrequency; - s.rewardToken = _rewardToken; - s.cooldownPeriod = _cooldownPeriod; - s.minStakeAmount = _minStakeAmount; - s.maxStakeAmount = _maxStakeAmount; - - emit StakingParametersUpdated( - _baseAPR, - _rewardDecayRate, - _compoundFrequency, - _rewardToken, - _cooldownPeriod, - _minStakeAmount, - _maxStakeAmount - ); - } - /** * @notice Retrieve staking parameters * @return baseAPR The base annual percentage rate for rewards. From b03d68a23ec1b61f6ccf39cb3a2017f6fb26444c Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Mon, 22 Dec 2025 10:57:29 +0800 Subject: [PATCH 16/20] Added unchecked --- src/token/Staking/StakingFacet.sol | 30 ++++++++++++++++++------------ src/token/Staking/StakingMod.sol | 30 ++++++++++++++++++------------ 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index a38fe962..9a60c8dc 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -531,11 +531,13 @@ contract StakingFacet { revert StakingUnsupportedToken(_tokenAddress); } - stake.amount += _value; - stake.stakedAt = block.timestamp; - stake.lastClaimedAt = block.timestamp; + unchecked { + stake.amount += _value; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; - s.totalStakedPerToken[_tokenAddress] += _value; + s.totalStakedPerToken[_tokenAddress] += _value; + } } /** @@ -553,11 +555,13 @@ contract StakingFacet { revert StakingUnsupportedToken(_tokenAddress); } - stake.amount = 1; - stake.stakedAt = block.timestamp; - stake.lastClaimedAt = block.timestamp; + unchecked { + stake.amount = 1; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; - s.totalStakedPerToken[_tokenAddress] += 1; + s.totalStakedPerToken[_tokenAddress] += 1; + } } /** @@ -576,11 +580,13 @@ contract StakingFacet { revert StakingUnsupportedToken(_tokenAddress); } - stake.amount += _value; - stake.stakedAt = block.timestamp; - stake.lastClaimedAt = block.timestamp; + unchecked { + stake.amount += _value; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; - s.totalStakedPerToken[_tokenAddress] += _value; + s.totalStakedPerToken[_tokenAddress] += _value; + } } /** diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol index 64ddd125..c5f96aa9 100644 --- a/src/token/Staking/StakingMod.sol +++ b/src/token/Staking/StakingMod.sol @@ -187,11 +187,13 @@ function stakeERC20(address _tokenAddress, uint256 _value) { revert StakingUnsupportedToken(_tokenAddress); } - stake.amount += _value; - stake.stakedAt = block.timestamp; - stake.lastClaimedAt = block.timestamp; + unchecked { + stake.amount += _value; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; - s.totalStakedPerToken[_tokenAddress] += _value; + s.totalStakedPerToken[_tokenAddress] += _value; + } } /** @@ -209,11 +211,13 @@ function stakeERC721(address _tokenAddress, uint256 _tokenId) { revert StakingUnsupportedToken(_tokenAddress); } - stake.amount = 1; - stake.stakedAt = block.timestamp; - stake.lastClaimedAt = block.timestamp; + unchecked { + stake.amount = 1; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; - s.totalStakedPerToken[_tokenAddress] += 1; + s.totalStakedPerToken[_tokenAddress] += 1; + } } /** @@ -232,11 +236,13 @@ function stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) { revert StakingUnsupportedToken(_tokenAddress); } - stake.amount += _value; - stake.stakedAt = block.timestamp; - stake.lastClaimedAt = block.timestamp; + unchecked { + stake.amount += _value; + stake.stakedAt = block.timestamp; + stake.lastClaimedAt = block.timestamp; - s.totalStakedPerToken[_tokenAddress] += _value; + s.totalStakedPerToken[_tokenAddress] += _value; + } } /** From 6e791c97151cedb373422b20d392d33a2505d713 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Mon, 22 Dec 2025 13:20:17 +0800 Subject: [PATCH 17/20] revise StakingMod --- src/token/Staking/StakingMod.sol | 134 ++++++------------ test/token/Staking/Staking.t.sol | 115 --------------- .../Staking/harnesses/LibStakingHarness.sol | 35 ----- 3 files changed, 40 insertions(+), 244 deletions(-) diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol index c5f96aa9..0a7d52d2 100644 --- a/src/token/Staking/StakingMod.sol +++ b/src/token/Staking/StakingMod.sol @@ -24,26 +24,6 @@ error StakingZeroStakeAmount(); */ error StakingOverflow(); -/** - * @notice Emitted when staking parameters are updated. - * @param baseAPR The base annual percentage rate for rewards. - * @param rewardDecayRate The decay rate for rewards over time. - * @param compoundFrequency The frequency at which rewards are compounded. - * @param rewardToken The address of the token used for rewards. - * @param cooldownPeriod The cooldown period before unstaking is allowed. - * @param minStakeAmount The minimum amount required to stake. - * @param maxStakeAmount The maximum amount allowed to stake. - */ -event StakingParametersUpdated( - uint256 baseAPR, - uint256 rewardDecayRate, - uint256 compoundFrequency, - address rewardToken, - uint256 cooldownPeriod, - uint256 minStakeAmount, - uint256 maxStakeAmount -); - /** * @notice Emitted when supported token types are added. */ @@ -127,51 +107,6 @@ function addSupportedToken(address _tokenAddress, bool _isERC20, bool _isERC721, return true; } -/** - * @notice An admin function to set staking parameters. - * @param _baseAPR The base annual percentage rate for rewards. - * @param _rewardDecayRate The decay rate for rewards over time. - * @param _compoundFrequency The frequency at which rewards are compounded. - * @param _rewardToken The address of the token used for rewards. - * @param _cooldownPeriod The cooldown period before unstaking is allowed. - * @param _minStakeAmount The minimum amount required to stake. - * @param _maxStakeAmount The maximum amount allowed to stake. - * @dev This function should be restricted to admin use only. - * @dev Emits a StakingParametersUpdated event upon successful update. - */ -function setStakingParameters( - uint256 _baseAPR, - uint256 _rewardDecayRate, - uint256 _compoundFrequency, - address _rewardToken, - uint256 _cooldownPeriod, - uint256 _minStakeAmount, - uint256 _maxStakeAmount -) { - StakingStorage storage s = getStorage(); - - bool isSupported = isTokenSupported(_rewardToken); - if (!isSupported) { - revert StakingUnsupportedToken(_rewardToken); - } - - if (_minStakeAmount == 0 || _maxStakeAmount == 0) { - revert StakingZeroStakeAmount(); - } - - s.baseAPR = _baseAPR; - s.rewardDecayRate = _rewardDecayRate; - s.compoundFrequency = _compoundFrequency; - s.rewardToken = _rewardToken; - s.cooldownPeriod = _cooldownPeriod; - s.minStakeAmount = _minStakeAmount; - s.maxStakeAmount = _maxStakeAmount; - - emit StakingParametersUpdated( - _baseAPR, _rewardDecayRate, _compoundFrequency, _rewardToken, _cooldownPeriod, _minStakeAmount, _maxStakeAmount - ); -} - /** * @notice Stake ERC-20 tokens * @dev Transfers token from the user and updates the amount staked and staking info. @@ -246,37 +181,48 @@ function stakeERC1155(address _tokenAddress, uint256 _tokenId, uint256 _value) { } /** - * @notice Retrieve staking parameters - * @return baseAPR The base annual percentage rate for rewards. - * @return rewardDecayRate The decay rate for rewards over time. - * @return compoundFrequency The frequency at which rewards are compounded. - * @return rewardToken The address of the token used for rewards. - * @return cooldownPeriod The cooldown period before unstaking is allowed. - * @return minStakeAmount The minimum amount required to stake. - * @return maxStakeAmount The maximum amount allowed to stake. + * @notice Calculates the rewards for a staked token. + * @dev Uses base APR, decay rate, and compounding frequency to compute rewards. + * @dev Rewards are calculated based on the time since last claim. + * @dev Uses fixed-point arithmetic with 1e18 precision. + * @param _tokenAddress The address of the staked token. + * @param _tokenId The ID of the staked token. + * @return finalReward The calculated reward amount. */ -function getStakingParameters() - view - returns ( - uint256 baseAPR, - uint256 rewardDecayRate, - uint256 compoundFrequency, - address rewardToken, - uint256 cooldownPeriod, - uint256 minStakeAmount, - uint256 maxStakeAmount - ) -{ +function calculateRewards(address _tokenAddress, uint256 _tokenId) view returns (uint256) { StakingStorage storage s = getStorage(); - return ( - s.baseAPR, - s.rewardDecayRate, - s.compoundFrequency, - s.rewardToken, - s.cooldownPeriod, - s.minStakeAmount, - s.maxStakeAmount - ); + StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; + + // Calculate staking duration + uint256 stakedDuration = block.timestamp - stake.lastClaimedAt; + if (stakedDuration == 0 || stake.amount == 0) { + return 0; + } + + // Base reward rate with decay + uint256 baseReward = (stake.amount * s.baseAPR * stakedDuration) / (365 days * 100); + + // Apply decay factor based on staking duration and compound frequency + uint256 decayFactor; + if (s.rewardDecayRate > 0 && s.compoundFrequency > 0) { + uint256 exponent = stakedDuration / s.compoundFrequency; + + /** + * Cap exponent to prevent overflow + * With max exponent of 125, even 2e18^125 is manageable with rpow + */ + if (exponent > 125) { + exponent = 125; + } + + decayFactor = rpow(s.rewardDecayRate, exponent); + } else { + decayFactor = 1e18; + } + + uint256 finalReward = (baseReward * decayFactor) / (10 ** 18); + + return finalReward; } /** diff --git a/test/token/Staking/Staking.t.sol b/test/token/Staking/Staking.t.sol index 634fcd9f..48882633 100644 --- a/test/token/Staking/Staking.t.sol +++ b/test/token/Staking/Staking.t.sol @@ -103,121 +103,6 @@ contract StakingTest is Test { erc721Token.mint(bob, TOKEN_ID_2); erc721Token.mint(alice, TOKEN_ID_1); rewardToken.mint(address(staking), 10_000 ether); - - /** - * Initialize staking parameters - */ - staking.initialize( - BASE_APR, - REWARD_DECAY_RATE, - COMPOUND_FREQUENCY, - address(rewardToken), - COOLDOWN_PERIOD, - MIN_STAKE_AMOUNT, - MAX_STAKE_AMOUNT - ); - } - - function test_ParametersAreSetCorrectly() public { - ( - uint256 baseAPR, - uint256 rewardDecayRate, - uint256 compoundFrequency, - address rewardTokenAddress, - uint256 cooldownPeriod, - uint256 minStakeAmount, - uint256 maxStakeAmount - ) = staking.getStakingParameters(); - - assertEq(baseAPR, BASE_APR); - assertEq(rewardDecayRate, REWARD_DECAY_RATE); - assertEq(compoundFrequency, COMPOUND_FREQUENCY); - assertEq(rewardTokenAddress, address(rewardToken)); - assertEq(cooldownPeriod, COOLDOWN_PERIOD); - assertEq(minStakeAmount, MIN_STAKE_AMOUNT); - assertEq(maxStakeAmount, MAX_STAKE_AMOUNT); - } - - function test_ParametersAreSetCorrectly_EventEmitted() public { - vm.expectEmit(true, true, true, true); - emit StakingParametersUpdated( - BASE_APR, - REWARD_DECAY_RATE, - COMPOUND_FREQUENCY, - address(rewardToken), - COOLDOWN_PERIOD, - MIN_STAKE_AMOUNT, - MAX_STAKE_AMOUNT - ); - - staking.initialize( - BASE_APR, - REWARD_DECAY_RATE, - COMPOUND_FREQUENCY, - address(rewardToken), - COOLDOWN_PERIOD, - MIN_STAKE_AMOUNT, - MAX_STAKE_AMOUNT - ); - } - - function test_ParametersAreSetCorrectly_RevertOnUnsupportedToken() public { - address unsupportedToken = makeAddr("unsupportedToken"); - - vm.expectRevert(abi.encodeWithSelector(StakingMod.StakingUnsupportedToken.selector, unsupportedToken)); - staking.initialize( - BASE_APR, - REWARD_DECAY_RATE, - COMPOUND_FREQUENCY, - unsupportedToken, - COOLDOWN_PERIOD, - MIN_STAKE_AMOUNT, - MAX_STAKE_AMOUNT - ); - } - - function test_ParametersAreSetCorrectly_RevertOnZeroStakeAmount() public { - uint256 newMinStakeAmount = 0; - vm.expectRevert(abi.encodeWithSelector(StakingMod.StakingZeroStakeAmount.selector, newMinStakeAmount)); - staking.initialize( - BASE_APR, - REWARD_DECAY_RATE, - COMPOUND_FREQUENCY, - address(rewardToken), - COOLDOWN_PERIOD, - newMinStakeAmount, - MAX_STAKE_AMOUNT - ); - - uint256 newMaxStakeAmount = 0; - vm.expectRevert(abi.encodeWithSelector(StakingMod.StakingZeroStakeAmount.selector, newMaxStakeAmount)); - staking.initialize( - BASE_APR, - REWARD_DECAY_RATE, - COMPOUND_FREQUENCY, - address(rewardToken), - COOLDOWN_PERIOD, - MIN_STAKE_AMOUNT, - newMaxStakeAmount - ); - } - - function test_ParametersAreSetCorrectly_EmitEventsSupportedToken() public { - vm.expectEmit(true, true, true, true); - emit SupportedTokenAdded(address(erc20Token), true, false, false); - staking.addSupportedToken(address(erc20Token), true, false, false); - - vm.expectEmit(true, true, true, true); - emit SupportedTokenAdded(address(erc721Token), false, true, false); - staking.addSupportedToken(address(erc721Token), false, true, false); - - vm.expectEmit(true, true, true, true); - emit SupportedTokenAdded(address(erc1155Token), false, false, true); - staking.addSupportedToken(address(erc1155Token), false, false, true); - - vm.expectEmit(true, true, true, true); - emit SupportedTokenAdded(address(rewardToken), true, false, false); - staking.addSupportedToken(address(rewardToken), true, false, false); } function test_VerifySupportedTokens() public { diff --git a/test/token/Staking/harnesses/LibStakingHarness.sol b/test/token/Staking/harnesses/LibStakingHarness.sol index ce5fd526..934b2ac4 100644 --- a/test/token/Staking/harnesses/LibStakingHarness.sol +++ b/test/token/Staking/harnesses/LibStakingHarness.sol @@ -9,30 +9,6 @@ import "../../../../src/token/Staking/StakingMod.sol" as StakingMod; * @dev Required for testing since StakingMod functions are internal */ contract LibStakingHarness { - /** - * @notice Initialize the staking storage for testing - * @dev Only used for testing purposes - */ - function initialize( - uint256 _baseAPR, - uint256 _rewardDecayRate, - uint256 _compoundFrequency, - address _rewardToken, - uint256 _cooldownPeriod, - uint256 _minStakeAmount, - uint256 _maxStakeAmount - ) external { - StakingMod.setStakingParameters( - _baseAPR, - _rewardDecayRate, - _compoundFrequency, - _rewardToken, - _cooldownPeriod, - _minStakeAmount, - _maxStakeAmount - ); - } - /** * @notice Exposes StakingMod.addSupportedToken as an external function */ @@ -61,17 +37,6 @@ contract LibStakingHarness { StakingMod.stakeERC1155(_tokenAddress, _tokenId, _value); } - /** - * @notice Exposes StakingMod.getStakingParameters as an external function - */ - function getStakingParameters() - external - view - returns (uint256, uint256, uint256, address, uint256, uint256, uint256) - { - return StakingMod.getStakingParameters(); - } - /** * @notice Exposes StakingMod.getStakedTokenInfo as an external function */ From ddeae599855b413fda58898006504f767e55cdcd Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Tue, 23 Dec 2025 05:18:41 +0800 Subject: [PATCH 18/20] remove comments --- test/token/Staking/StakingFacet.t.sol | 79 +++++++-------------------- 1 file changed, 21 insertions(+), 58 deletions(-) diff --git a/test/token/Staking/StakingFacet.t.sol b/test/token/Staking/StakingFacet.t.sol index d1895f78..1775775e 100644 --- a/test/token/Staking/StakingFacet.t.sol +++ b/test/token/Staking/StakingFacet.t.sol @@ -148,7 +148,6 @@ contract StakingFacetTest is Test { facet.addSupportedToken(address(rewardToken), true, false, false); - // Expect event vm.expectEmit(true, true, true, true); emit StakingParametersUpdated( BASE_APR, @@ -160,7 +159,6 @@ contract StakingFacetTest is Test { MAX_STAKE_AMOUNT ); - // Re-initialize to trigger event facet.setStakingParameters( BASE_APR, REWARD_DECAY_RATE, @@ -177,18 +175,12 @@ contract StakingFacetTest is Test { function test_StakeERC20Token() public { vm.startPrank(alice); - // Approve the staking contract to spend Alice's tokens erc20Token.approve(address(facet), 500 ether); - // Stake tokens facet.stakeToken(address(erc20Token), 0, 500 ether); - // Verify staking state (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) = - facet.getStakedTokenInfo( - address(erc20Token), - 0 // Token ID is 0 for ERC-20 staking - ); + facet.getStakedTokenInfo(address(erc20Token), 0); assertEq(amount, 500 ether); assertGt(stakedAt, 0); @@ -201,13 +193,10 @@ contract StakingFacetTest is Test { function test_StakeERC721Token() public { vm.startPrank(bob); - // Approve the staking contract to transfer Bob's NFT erc721Token.approve(address(facet), TOKEN_ID_2); - // Stake NFT facet.stakeToken(address(erc721Token), TOKEN_ID_2, 1); - // Verify staking state (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) = facet.getStakedTokenInfo(address(erc721Token), TOKEN_ID_2); @@ -222,13 +211,10 @@ contract StakingFacetTest is Test { function test_StakeERC1155Token() public { vm.startPrank(alice); - // Approve the staking contract to transfer Alice's ERC-1155 tokens erc1155Token.setApprovalForAll(address(facet), true); - // Stake ERC-1155 tokens facet.stakeToken(address(erc1155Token), TOKEN_ID_1, 5); - // Verify staking state (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) = facet.getStakedTokenInfo(address(erc1155Token), TOKEN_ID_1); @@ -243,14 +229,11 @@ contract StakingFacetTest is Test { function test_StakeToken_EmitTokensStakedEvent() public { vm.startPrank(alice); - // Approve the staking contract to spend Alice's tokens erc20Token.approve(address(facet), 500 ether); - // Expect event vm.expectEmit(true, true, true, true); emit TokensStaked(alice, address(erc20Token), 0, 500 ether); - // Stake tokens facet.stakeToken(address(erc20Token), 0, 500 ether); vm.stopPrank(); @@ -292,7 +275,6 @@ contract StakingFacetTest is Test { function test_StakeERC721Token_RevertsIfNotOwner() public { vm.startPrank(bob); - // Bob tries to stake Alice's NFT vm.expectRevert( abi.encodeWithSelector(StakingFacet.StakingNotTokenOwner.selector, bob, address(erc721Token), TOKEN_ID_1) ); @@ -304,7 +286,6 @@ contract StakingFacetTest is Test { function test_StakeERC1155Token_RevertsIfNotEnoughBalance() public { vm.startPrank(bob); - // Bob tries to stake more ERC-1155 tokens than he owns vm.expectRevert(abi.encodeWithSelector(StakingFacet.StakingInsufficientBalance.selector, bob, 10, 20)); facet.stakeToken(address(erc1155Token), TOKEN_ID_2, 20); @@ -314,17 +295,13 @@ contract StakingFacetTest is Test { function test_UnstakeERC20Token() public { vm.startPrank(alice); - // Approve and stake tokens erc20Token.approve(address(facet), 500 ether); facet.stakeToken(address(erc20Token), 0, 500 ether); - // Warp time to pass cooldown period vm.warp(block.timestamp + COOLDOWN_PERIOD + 1); - // Unstake tokens facet.unstakeToken(address(erc20Token), 0); - // Verify staking state is reset (uint256 amount,,,) = facet.getStakedTokenInfo(address(erc20Token), 0); assertEq(amount, 0); @@ -334,17 +311,13 @@ contract StakingFacetTest is Test { function test_UnstakeERC721Token() public { vm.startPrank(bob); - // Approve and stake NFT erc721Token.approve(address(facet), TOKEN_ID_2); facet.stakeToken(address(erc721Token), TOKEN_ID_2, 1); - // Warp time to pass cooldown period vm.warp(block.timestamp + COOLDOWN_PERIOD + 1); - // Unstake NFT facet.unstakeToken(address(erc721Token), TOKEN_ID_2); - // Verify staking state is reset (uint256 amount,,,) = facet.getStakedTokenInfo(address(erc721Token), TOKEN_ID_2); assertEq(amount, 0); @@ -354,17 +327,13 @@ contract StakingFacetTest is Test { function test_UnstakeERC1155Token() public { vm.startPrank(alice); - // Approve and stake ERC-1155 tokens erc1155Token.setApprovalForAll(address(facet), true); facet.stakeToken(address(erc1155Token), TOKEN_ID_1, 5); - // Warp time to pass cooldown period vm.warp(block.timestamp + COOLDOWN_PERIOD + 1); - // Unstake ERC-1155 tokens facet.unstakeToken(address(erc1155Token), TOKEN_ID_1); - // Verify staking state is reset (uint256 amount,,,) = facet.getStakedTokenInfo(address(erc1155Token), TOKEN_ID_1); assertEq(amount, 0); @@ -374,18 +343,14 @@ contract StakingFacetTest is Test { function test_UnstakeToken_EmitTokensUnstakedEvent() public { vm.startPrank(alice); - // Approve and stake tokens erc20Token.approve(address(facet), 500 ether); facet.stakeToken(address(erc20Token), 0, 500 ether); - // Warp time to pass cooldown period vm.warp(block.timestamp + COOLDOWN_PERIOD + 1); - // Expect event vm.expectEmit(true, true, true, true); emit TokensUnstaked(alice, address(erc20Token), 0, 500 ether); - // Unstake tokens facet.unstakeToken(address(erc20Token), 0); vm.stopPrank(); @@ -431,7 +396,7 @@ contract StakingFacetTest is Test { uint256 calculatedRewards = facet.calculateRewardsForToken(address(erc20Token), 0); - assertGt(calculatedRewards, 0); // 1000 ether * 10% * 1 year = 100 ether + assertGt(calculatedRewards, 0); vm.stopPrank(); } @@ -450,16 +415,15 @@ contract StakingFacetTest is Test { (uint256 amount, uint256 stakedAt, uint256 lastClaimedAt, uint256 accumulatedRewards) = facet.getStakedTokenInfo(address(erc20Token), 0); - assertGt(rewardsBalanceAfter, rewardsBalanceBefore); // Alice should have received rewards + assertGt(rewardsBalanceAfter, rewardsBalanceBefore); assertEq(accumulatedRewards, rewardsBalanceAfter - rewardsBalanceBefore); assertEq(lastClaimedAt, block.timestamp); - assertEq(amount, 1000 ether); // Staked amount should remain unchanged + assertEq(amount, 1000 ether); vm.stopPrank(); } - // Fuzz Tests for ERC-20 Staking function testFuzz_StakeAmount(uint256 stakeAmount) public { - stakeAmount = bound(stakeAmount, 1e18, 1_000_000e18); // Clamp between 1 and 1,000,000 tokens + stakeAmount = bound(stakeAmount, 1e18, 1_000_000e18); vm.startPrank(owner); facet.addSupportedToken(address(erc20Token), true, false, false); @@ -479,9 +443,8 @@ contract StakingFacetTest is Test { vm.stopPrank(); } - // Fuzz Test for long staking durations function testFuzz_RewardAfterTime(uint256 daysStaked) public { - daysStaked = bound(daysStaked, 1, 3650); // 1 day to 10 years + daysStaked = bound(daysStaked, 1, 3650); vm.startPrank(owner); facet.addSupportedToken(address(erc20Token), true, false, false); @@ -500,16 +463,16 @@ contract StakingFacetTest is Test { uint256 rewards = facet.calculateRewardsForToken(address(erc20Token), 0); assertGt(rewards, 0); - assertLe(rewards, 1_000_000 ether); // Should not exceed total reward pool + assertLe(rewards, 1_000_000 ether); vm.stopPrank(); } function testFuzz_RewardDecay(uint256 stakedSeconds, uint256 decayRate, uint256 compoundFreq) public { vm.startPrank(owner); - // Clamp fuzzed values to safe bounds - stakedSeconds = bound(stakedSeconds, 1 days, 3650 days); // 1 day to 10 years - decayRate = bound(decayRate, 0.5e18, 1.5e18); // 50% to 150% - compoundFreq = bound(compoundFreq, 1 days, 365 days); // 1 day to 1 year + + stakedSeconds = bound(stakedSeconds, 1 days, 3650 days); + decayRate = bound(decayRate, 0.5e18, 1.5e18); + compoundFreq = bound(compoundFreq, 1 days, 365 days); facet.addSupportedToken(address(erc20Token), true, false, false); facet.addSupportedToken(address(rewardToken), true, false, false); @@ -547,8 +510,8 @@ contract StakingFacetTest is Test { } function testFuzz_StakeUnstake(uint256 stakeAmount, uint256 waitDays) public { - stakeAmount = bound(stakeAmount, 1e18, 1_000e18); // 1 to 1,000 tokens - waitDays = bound(waitDays, 1, 365); // 1 to 365 days + stakeAmount = bound(stakeAmount, 1e18, 1_000e18); + waitDays = bound(waitDays, 1, 365); vm.startPrank(owner); facet.addSupportedToken(address(erc20Token), true, false, false); @@ -581,30 +544,30 @@ contract StakingFacetTest is Test { } function test_FixedPoint_IntegerPower() public { - uint256 result = facet.rPow(2e18, 3); // 2^3 = 8 + uint256 result = facet.rPow(2e18, 3); assertEq(result, 8e18); - result = facet.rPow(5e18, 0); // 5^0 = 1 + result = facet.rPow(5e18, 0); assertEq(result, 1e18); - result = facet.rPow(1e18, 10); // 1^10 = 1 + result = facet.rPow(1e18, 10); assertEq(result, 1e18); - result = facet.rPow(3e18, 4); // 3^4 = 81 + result = facet.rPow(3e18, 4); assertEq(result, 81e18); } function test_FixedPoint_Multiply() public { - uint256 result = facet.rMul(2e18, 3e18); // 2 * 3 = 6 + uint256 result = facet.rMul(2e18, 3e18); assertEq(result, 6e18); - result = facet.rMul(5e18, 0e18); // 5 * 0 = 0 + result = facet.rMul(5e18, 0e18); assertEq(result, 0); - result = facet.rMul(1e18, 10e18); // 1 * 10 = 10 + result = facet.rMul(1e18, 10e18); assertEq(result, 10e18); - result = facet.rMul(3e18, 4e18); // 3 * 4 = 12 + result = facet.rMul(3e18, 4e18); assertEq(result, 12e18); } } From 439d8e9f49baa325ce675b3d5cb3512292a4902c Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Tue, 23 Dec 2025 07:28:13 +0800 Subject: [PATCH 19/20] remove comments --- src/token/Staking/StakingFacet.sol | 22 ++++++++++-------- src/token/Staking/StakingMod.sol | 3 --- test/token/Staking/Staking.t.sol | 33 ++++++++++----------------- test/token/Staking/StakingFacet.t.sol | 7 +++--- 4 files changed, 27 insertions(+), 38 deletions(-) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index 9a60c8dc..33d29efd 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -373,6 +373,7 @@ contract StakingFacet { bool isSupported = isTokenSupported(_tokenAddress); bool isTokenERC20 = s.supportedTokens[_tokenAddress].isERC20; + bool isTokenERC1155 = s.supportedTokens[_tokenAddress].isERC1155; if (!isSupported) { revert StakingUnsupportedToken(_tokenAddress); @@ -602,16 +603,13 @@ contract StakingFacet { StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; - // Calculate staking duration uint256 stakedDuration = block.timestamp - stake.lastClaimedAt; if (stakedDuration == 0 || stake.amount == 0) { return 0; } - // Base reward rate with decay uint256 baseReward = (stake.amount * s.baseAPR * stakedDuration) / (365 days * 100); - // Apply decay factor based on staking duration and compound frequency uint256 decayFactor; if (s.rewardDecayRate > 0 && s.compoundFrequency > 0) { uint256 exponent = stakedDuration / s.compoundFrequency; @@ -679,7 +677,7 @@ contract StakingFacet { * @return result Fixed-point result of base^exp, scaled by 1e18. */ function rpow(uint256 _base, uint256 _exp) internal pure returns (uint256 result) { - result = 1e18; // Initialize result as 1 in 1e18 fixed-point + result = 1e18; uint256 base = _base; while (_exp > 0) { @@ -725,9 +723,11 @@ contract StakingFacet { return interfaceId == type(IERC721Receiver).interfaceId || interfaceId == type(IERC1155Receiver).interfaceId; } - /// @notice Handle the receipt of an NFT - /// @dev The ERC721 smart contract calls this on the recipient after a `safeTransfer`. - /// @return The selector to confirm token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + /** + * @notice Handle the receipt of an NFT + * @dev The ERC721 smart contract calls this on the recipient after a `safeTransfer`. + * @return The selector to confirm token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + */ function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external pure @@ -736,9 +736,11 @@ contract StakingFacet { return IERC721Receiver.onERC721Received.selector; } - /// @notice Handle the receipt of a single ERC1155 token type - /// @dev The ERC1155 smart contract calls this on the recipient after a `safeTransferFrom`. - /// @return The selector to confirm token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + /** + * @notice Handle the receipt of a single ERC1155 token type + * @dev The ERC1155 smart contract calls this on the recipient after a `safeTransferFrom`. + * @return The selector to confirm token transfer. If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted. + */ function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes calldata data) external pure diff --git a/src/token/Staking/StakingMod.sol b/src/token/Staking/StakingMod.sol index 0a7d52d2..66ee1726 100644 --- a/src/token/Staking/StakingMod.sol +++ b/src/token/Staking/StakingMod.sol @@ -193,16 +193,13 @@ function calculateRewards(address _tokenAddress, uint256 _tokenId) view returns StakingStorage storage s = getStorage(); StakedTokenInfo storage stake = s.stakedTokens[msg.sender][_tokenAddress][_tokenId]; - // Calculate staking duration uint256 stakedDuration = block.timestamp - stake.lastClaimedAt; if (stakedDuration == 0 || stake.amount == 0) { return 0; } - // Base reward rate with decay uint256 baseReward = (stake.amount * s.baseAPR * stakedDuration) / (365 days * 100); - // Apply decay factor based on staking duration and compound frequency uint256 decayFactor; if (s.rewardDecayRate > 0 && s.compoundFrequency > 0) { uint256 exponent = stakedDuration / s.compoundFrequency; diff --git a/test/token/Staking/Staking.t.sol b/test/token/Staking/Staking.t.sol index 48882633..73f6517e 100644 --- a/test/token/Staking/Staking.t.sol +++ b/test/token/Staking/Staking.t.sol @@ -1,15 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.30; -import {Test, console} from "forge-std/Test.sol"; +import {Test} from "forge-std/Test.sol"; import {LibStakingHarness} from "./harnesses/LibStakingHarness.sol"; -import {ERC20FacetHarness} from "../ERC20//ERC20/harnesses/ERC20FacetHarness.sol"; +import {ERC20FacetHarness} from "../ERC20/ERC20/harnesses/ERC20FacetHarness.sol"; import {ERC721FacetHarness} from "../ERC721/ERC721/harnesses/ERC721FacetHarness.sol"; import {ERC1155FacetHarness} from "../ERC1155/ERC1155/harnesses/ERC1155FacetHarness.sol"; import "../../../src/token/Staking/StakingMod.sol" as StakingMod; -// import "forge-std/console.sol"; - contract StakingTest is Test { ERC20FacetHarness erc20Token; ERC20FacetHarness rewardToken; @@ -33,8 +31,8 @@ contract StakingTest is Test { uint256 constant TOKEN_ID_1 = 1; uint256 constant TOKEN_ID_2 = 2; - uint256 constant BASE_APR = 500; // 5% - uint256 constant REWARD_DECAY_RATE = 50; // 0.5% + uint256 constant BASE_APR = 500; + uint256 constant REWARD_DECAY_RATE = 50; uint256 constant COMPOUND_FREQUENCY = 1 days; uint256 constant COOLDOWN_PERIOD = 7 days; uint256 constant MIN_STAKE_AMOUNT = 1 ether; @@ -106,7 +104,6 @@ contract StakingTest is Test { } function test_VerifySupportedTokens() public { - // Supported tokens bool isERC20Supported = staking.isTokenSupported(address(erc20Token)); bool isERC721Supported = staking.isTokenSupported(address(erc721Token)); bool isERC1155Supported = staking.isTokenSupported(address(erc1155Token)); @@ -117,7 +114,6 @@ contract StakingTest is Test { assertTrue(isERC1155Supported); assertTrue(isRewardTokenSupported); - // Unsupported token address unsupportedToken = makeAddr("unsupportedToken"); bool isUnsupportedTokenSupported = staking.isTokenSupported(unsupportedToken); assertFalse(isUnsupportedTokenSupported); @@ -129,7 +125,6 @@ contract StakingTest is Test { erc20Token.approve(address(staking), stakeAmount); - // Stake ERC-20 tokens staking.stakeERC20(address(erc20Token), stakeAmount); (uint256 amount,,,) = staking.getStakedTokenInfo(address(erc20Token), 0); @@ -141,11 +136,9 @@ contract StakingTest is Test { function test_StakeERC721UpdatesState() public { vm.startPrank(alice); - // Stake ERC-721 token staking.stakeERC721(address(erc721Token), TOKEN_ID_1); (uint256 amount,,,) = staking.getStakedTokenInfo(address(erc721Token), TOKEN_ID_1); - console.log("Staked amount:", amount); assertEq(amount, 1); vm.stopPrank(); @@ -157,11 +150,9 @@ contract StakingTest is Test { erc1155Token.setApprovalForAll(address(staking), true); - // Stake ERC-1155 tokens staking.stakeERC1155(address(erc1155Token), TOKEN_ID_2, stakeAmount); (uint256 amount,,,) = staking.getStakedTokenInfo(address(erc1155Token), TOKEN_ID_2); - console.log("Staked amount:", amount); assertEq(amount, stakeAmount); vm.stopPrank(); @@ -175,30 +166,30 @@ contract StakingTest is Test { } function test_FixedPoint_IntegerPower() public { - uint256 result = staking.rPow(2e18, 3); // 2^3 = 8 + uint256 result = staking.rPow(2e18, 3); assertEq(result, 8e18); - result = staking.rPow(5e18, 0); // 5^0 = 1 + result = staking.rPow(5e18, 0); assertEq(result, 1e18); - result = staking.rPow(1e18, 10); // 1^10 = 1 + result = staking.rPow(1e18, 10); assertEq(result, 1e18); - result = staking.rPow(3e18, 4); // 3^4 = 81 + result = staking.rPow(3e18, 4); assertEq(result, 81e18); } function test_FixedPoint_Multiply() public { - uint256 result = staking.rMul(2e18, 3e18); // 2 * 3 = 6 + uint256 result = staking.rMul(2e18, 3e18); assertEq(result, 6e18); - result = staking.rMul(5e18, 0e18); // 5 * 0 = 0 + result = staking.rMul(5e18, 0e18); assertEq(result, 0); - result = staking.rMul(1e18, 10e18); // 1 * 10 = 10 + result = staking.rMul(1e18, 10e18); assertEq(result, 10e18); - result = staking.rMul(3e18, 4e18); // 3 * 4 = 12 + result = staking.rMul(3e18, 4e18); assertEq(result, 12e18); } } diff --git a/test/token/Staking/StakingFacet.t.sol b/test/token/Staking/StakingFacet.t.sol index 1775775e..dd7d39e2 100644 --- a/test/token/Staking/StakingFacet.t.sol +++ b/test/token/Staking/StakingFacet.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.30; import {Test} from "forge-std/Test.sol"; import {StakingFacetHarness} from "./harnesses/StakingFacetHarness.sol"; import {StakingFacet} from "../../../src/token/Staking/StakingFacet.sol"; -import {ERC20FacetHarness} from "../ERC20//ERC20/harnesses/ERC20FacetHarness.sol"; +import {ERC20FacetHarness} from "../ERC20/ERC20/harnesses/ERC20FacetHarness.sol"; import {ERC721FacetHarness} from "../ERC721/ERC721/harnesses/ERC721FacetHarness.sol"; import {ERC1155FacetHarness} from "../ERC1155/ERC1155/harnesses/ERC1155FacetHarness.sol"; import {IERC721Receiver} from "../../../src/interfaces/IERC721Receiver.sol"; @@ -34,8 +34,8 @@ contract StakingFacetTest is Test { uint256 constant TOKEN_ID_1 = 1; uint256 constant TOKEN_ID_2 = 2; - uint256 constant BASE_APR = 10; // 10% - uint256 constant REWARD_DECAY_RATE = 0; // no decay + uint256 constant BASE_APR = 10; + uint256 constant REWARD_DECAY_RATE = 0; uint256 constant COMPOUND_FREQUENCY = 365 days; uint256 constant COOLDOWN_PERIOD = 1 days; uint256 constant MIN_STAKE_AMOUNT = 1 ether; @@ -477,7 +477,6 @@ contract StakingFacetTest is Test { facet.addSupportedToken(address(erc20Token), true, false, false); facet.addSupportedToken(address(rewardToken), true, false, false); - // set the staking parameters facet.setStakingParameters( 1000, decayRate, compoundFreq, address(rewardToken), COOLDOWN_PERIOD, 1, type(uint256).max ); From 6dc6b60b5a3a176f7ff00e40a0f9fa203c2d2ed1 Mon Sep 17 00:00:00 2001 From: 0xhaz Date: Tue, 23 Dec 2025 08:13:18 +0800 Subject: [PATCH 20/20] remove bool isERC1155 --- src/token/Staking/StakingFacet.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/token/Staking/StakingFacet.sol b/src/token/Staking/StakingFacet.sol index 33d29efd..0407db5d 100644 --- a/src/token/Staking/StakingFacet.sol +++ b/src/token/Staking/StakingFacet.sol @@ -373,7 +373,6 @@ contract StakingFacet { bool isSupported = isTokenSupported(_tokenAddress); bool isTokenERC20 = s.supportedTokens[_tokenAddress].isERC20; - bool isTokenERC1155 = s.supportedTokens[_tokenAddress].isERC1155; if (!isSupported) { revert StakingUnsupportedToken(_tokenAddress);