Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 52 additions & 23 deletions src/Hyperstaker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra
event RewardClaimed(uint256 indexed hypercertId, uint256 reward);
event RewardSet(address indexed token, uint256 amount);

modifier onlyStaker(uint256 _hypercertId) {
require(stakes[_hypercertId].stakingStartTime != 0, NotStaked());
address staker = stakes[_hypercertId].staker;
require(staker == msg.sender, NotStakerOfHypercert(staker));
_;
}
Comment on lines +57 to +62
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

❌ Custom-error syntax breaks compilation

require only accepts a boolean condition and an optional string message.
Passing a custom error identifier as the second argument (require(condition, NotStaked());) is invalid Solidity syntax and will not compile.

modifier onlyStaker(uint256 _hypercertId) {
-    require(stakes[_hypercertId].stakingStartTime != 0, NotStaked());
-    address staker = stakes[_hypercertId].staker;
-    require(staker == msg.sender, NotStakerOfHypercert(staker));
+    if (stakes[_hypercertId].stakingStartTime == 0) revert NotStaked();
+    address staker = stakes[_hypercertId].staker;
+    if (staker != msg.sender) revert NotStakerOfHypercert(staker);
     _;
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
modifier onlyStaker(uint256 _hypercertId) {
require(stakes[_hypercertId].stakingStartTime != 0, NotStaked());
address staker = stakes[_hypercertId].staker;
require(staker == msg.sender, NotStakerOfHypercert(staker));
_;
}
modifier onlyStaker(uint256 _hypercertId) {
if (stakes[_hypercertId].stakingStartTime == 0) revert NotStaked();
address staker = stakes[_hypercertId].staker;
if (staker != msg.sender) revert NotStakerOfHypercert(staker);
_;
}


constructor() {
_disableInitializers();
}
Expand Down Expand Up @@ -96,7 +103,8 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra

// ADMIN FUNCTIONS

/// @notice Set the reward for the current round, this ends the current round and starts a new one. Only callable by a manager
/// @notice Set the reward for the current round, this ends the current round and starts a new one. Only callable
/// by a manager
/// @param _rewardToken address of the reward token
/// @param _rewardAmount amount of the reward for the current round
function setReward(address _rewardToken, uint256 _rewardAmount) external payable onlyRole(MANAGER_ROLE) {
Expand Down Expand Up @@ -129,7 +137,7 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra

// USER FUNCTIONS

/// @notice Stake a Hypercert, this will transfer the Hypercert from the user to the contract
/// @notice Stake a Hypercert, this will transfer the Hypercert from the owner to the contract
/// @param _hypercertId id of the Hypercert to stake
function stake(uint256 _hypercertId) external whenNotPaused {
require(hypercertMinter.unitsOf(_hypercertId) != 0, NoUnitsInHypercert());
Expand All @@ -143,38 +151,33 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra
}

/// @notice Unstake a Hypercert, this will transfer the Hypercert from the contract to the user and delete all
///stake information
/// stake information
/// @param _hypercertId id of the Hypercert to unstake
function unstake(uint256 _hypercertId) external whenNotPaused {
require(stakes[_hypercertId].stakingStartTime != 0, NotStaked());
address staker = stakes[_hypercertId].staker;
require(staker == msg.sender, NotStakerOfHypercert(staker));
delete stakes[_hypercertId];
emit Unstaked(_hypercertId);
hypercertMinter.safeTransferFrom(address(this), msg.sender, _hypercertId, 1, "");
function unstake(uint256 _hypercertId) public whenNotPaused onlyStaker(_hypercertId) {
_unstake(_hypercertId);
}

/// @notice Claim a reward eligable by a staked Hypercert for a given round
/// @param _hypercertId id of the Hypercert to claim the reward for
/// @param _roundId id of the round to claim the reward for
function claimReward(uint256 _hypercertId, uint256 _roundId) external whenNotPaused {
require(stakes[_hypercertId].stakingStartTime != 0, NotStaked());
address staker = stakes[_hypercertId].staker;
require(staker == msg.sender, NotStakerOfHypercert(staker));
function claimReward(uint256 _hypercertId, uint256 _roundId) external whenNotPaused onlyStaker(_hypercertId) {
require(!isRoundClaimed(_hypercertId, _roundId), AlreadyClaimed());
uint256 reward = _calculateReward(_hypercertId, _roundId);
require(reward != 0, NoRewardAvailable());

_setRoundClaimed(_hypercertId, _roundId);
emit RewardClaimed(_hypercertId, reward);
_claimReward(_hypercertId, _roundId, reward);
}

address rewardToken = rounds[_roundId].rewardToken;
if (rewardToken != address(0)) {
require(IERC20(rewardToken).transfer(msg.sender, reward), RewardTransferFailed());
} else {
(bool success,) = payable(msg.sender).call{value: reward}("");
require(success, NativeTokenTransferFailed());
/// @notice Claim all rewards available for a staked Hypercert and unstake it
/// @param _hypercertId id of the Hypercert to claim all rewards and unstake
function claimAndUnstake(uint256 _hypercertId) external whenNotPaused onlyStaker(_hypercertId) {
for (uint256 i = 0; i < rounds.length - 1; i++) {
uint256 reward = calculateReward(_hypercertId, i);
if (reward != 0) {
_claimReward(_hypercertId, i, reward);
}
}
unstake(_hypercertId);
}

// VIEW FUNCTIONS
Expand All @@ -184,6 +187,7 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra
/// @param _roundId id of the round to calculate the reward for
/// @return amount of the reward eligable for the staked Hypercert for the given round
function calculateReward(uint256 _hypercertId, uint256 _roundId) public view returns (uint256) {
require(stakes[_hypercertId].stakingStartTime != 0, NotStaked());
if (isRoundClaimed(_hypercertId, _roundId)) {
Comment on lines +190 to 191
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same compilation issue in calculateReward

The new check mirrors the pattern above and needs the same fix.

-        require(stakes[_hypercertId].stakingStartTime != 0, NotStaked());
+        if (stakes[_hypercertId].stakingStartTime == 0) revert NotStaked();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
require(stakes[_hypercertId].stakingStartTime != 0, NotStaked());
if (isRoundClaimed(_hypercertId, _roundId)) {
if (stakes[_hypercertId].stakingStartTime == 0) revert NotStaked();
if (isRoundClaimed(_hypercertId, _roundId)) {

return 0;
}
Expand Down Expand Up @@ -222,13 +226,38 @@ contract Hyperstaker is AccessControlUpgradeable, PausableUpgradeable, UUPSUpgra
Round memory round = rounds[_roundId];
require(round.endTime != 0, RoundNotSet());
uint256 stakeStartTime = stakes[_hypercertId].stakingStartTime;
require(stakeStartTime != 0, NotStaked());
stakeStartTime = stakeStartTime < round.startTime ? round.startTime : stakeStartTime;
uint256 stakeDuration = stakeStartTime > round.endTime ? 0 : round.endTime - stakeStartTime;
return
round.totalRewards * hypercertMinter.unitsOf(_hypercertId) * stakeDuration / (totalUnits * round.duration);
}

/// @notice Unstake a Hypercert, this will transfer the Hypercert from the contract to the user and delete all
/// stake information
/// @param _hypercertId id of the Hypercert to unstake
function _unstake(uint256 _hypercertId) internal {
delete stakes[_hypercertId];
emit Unstaked(_hypercertId);
hypercertMinter.safeTransferFrom(address(this), msg.sender, _hypercertId, 1, "");
}

/// @notice Set a round as claimed for a staked Hypercert and transfer the reward to the user
/// @param _hypercertId id of the Hypercert to claim the reward for
/// @param _roundId id of the round to claim the reward for
/// @param _reward amount of the reward to claim
function _claimReward(uint256 _hypercertId, uint256 _roundId, uint256 _reward) internal {
_setRoundClaimed(_hypercertId, _roundId);
emit RewardClaimed(_hypercertId, _reward);

address rewardToken = rounds[_roundId].rewardToken;
if (rewardToken != address(0)) {
require(IERC20(rewardToken).transfer(msg.sender, _reward), RewardTransferFailed());
} else {
Comment on lines +253 to +255
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prefer SafeERC20 for wide-token compatibility

Some ERC-20 tokens (e.g. USDT, certain roll-ups) don’t return a boolean or
otherwise deviate from the ERC-20 spec, causing IERC20.transfer(...) to revert
or return misleading values.
Using OpenZeppelin’s SafeERC20 handles these edge-cases and reduces the chance
of silent transfer failures.

-import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";-            require(IERC20(rewardToken).transfer(msg.sender, _reward), RewardTransferFailed());
+            SafeERC20.safeTransfer(IERC20(rewardToken), msg.sender, _reward);

(Remember to add using SafeERC20 for IERC20;.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (rewardToken != address(0)) {
require(IERC20(rewardToken).transfer(msg.sender, _reward), RewardTransferFailed());
} else {
// At the top of src/Hyperstaker.sol, update your imports and add the `using` directive:
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
// … later in your function …
if (rewardToken != address(0)) {
// SafeERC20 will handle non-standard ERC-20s and revert on failure
SafeERC20.safeTransfer(IERC20(rewardToken), msg.sender, _reward);
} else {
// …

(bool success,) = payable(msg.sender).call{value: _reward}("");
require(success, NativeTokenTransferFailed());
}
}

/// @notice Get the hypercert type id for a given hypercert id
/// @param _hypercertId id of the Hypercert to get the type id for
/// @return hypercert type id for the given hypercert id
Expand Down
Loading