Organization
- @kinetiq
Engagement Type
Spearbit Web3
Period
-
Repositories
Researchers
Findings
High Risk
1 findings
1 fixed
0 acknowledged
Medium Risk
1 findings
0 fixed
1 acknowledged
Low Risk
17 findings
7 fixed
10 acknowledged
Informational
8 findings
3 fixed
5 acknowledged
High Risk1 finding
Operator bond can not be recovered and will be locked in ExManager
Description
ExManagerrequires operators to deposit HYPE tokens equal toopBondas a “skin-in-the-game” mechanism. When an operator callsExManager.bond(), the contract deposits the HYPE bond and mints the corresponding amount ofexLSTtokens. TheseexLSTtokens are minted to theExManagercontract itself, not to the operator, ensuring that the operator cannot withdraw or use the bonded funds until the contract is unwound.In the current implementation, however, once the contract is unwound, the operator has no mechanism to redeem or claim back their HYPE bond. The
exLSTtokens minted toExManagerremain locked insideHIP3StakingManager, making the operator bond permanently inaccessible.This results in the bonded assets being irrecoverably locked, which contradicts the intended lifecycle of the operator bond and creates a significant economic burden for operators.
While the impact is limited for the upcoming permissioned deployment—where
opBondis relatively small—it becomes severe in the permissionless version, whereopBondmay be as high as 10% of 500,000 HYPE, potentially resulting in substantial permanent losses per operator.Recommendation
Consider allowing the operator to burn the corresponding amount of exLST tokens and claim back the HYPE bond after unwinding.
Medium Risk1 finding
Withdrawal Delay Can Be Bypassed When L1 Operations Processed More Than Once Per 24 Hours
State
- Acknowledged
Severity
- Severity: Medium
≈
Likelihood: Low×
Impact: High Submitted by
kamensec
Description
HyperCore enforces a 24-hour cooldown after staking before withdrawals from the same validator can be processed. The
StakingManagerprocesses L1 operations (withdrawals first, then deposits) viaprocessL1Operations(), but there is no on-chain enforcement that this function is called at most once per 24 hours.If
processL1Operations()is called more frequently than every 24 hours, the following scenario can occur:- User queues a withdrawal at T=0
- Operator processes L1 operations at T=0 (withdrawal sent to L1)
- L1 withdrawal fails because a deposit was made within last 24 hours
- Operator retries with
queueL1Operation()at T=6 days (withdrawal now succeeds on L1) - User calls
confirmWithdrawal()at T=7 days
The issue is that the withdrawal delay check in
confirmWithdrawal()uses the originalrequest.timestampfrom when the withdrawal was queued:// src/EXManager.sol (confirmWithdrawal)if (block.timestamp < request.timestamp + withdrawalDelay) { revert Errors.WithdrawalDelayNotMet();}When the operator retries a failed L1 operation, the
request.timestampis NOT updated. This means:- Original queue time: T=0
- Retry succeeds: T=6 days
- User confirms: T=7 days (only 1 day after actual L1 processing)
The 7-day withdrawal delay was intended to ensure funds are fully available on L1 before user confirmation, but if the L1 operation was delayed and retried, the actual L1 processing may have occurred much later than the original queue timestamp.
Impact Explanation
Users may be able to confirm withdrawals before the full intended delay has passed from the actual L1 operation.
Likelihood Explanation
Low likelihood because:
- Operator is a trusted role expected to follow operational guidelines
- L1 operation failures that require retry are edge cases
- The current implementation relies on off-chain operational discipline
However, the scenario becomes more likely if:
- Multiple operators or automated systems process operations
- L1 congestion causes frequent operation failures
- Operational mistakes lead to processing more than once per day
Recommendation
Add an on-chain cooldown enforcement for
processL1Operations()
Low Risk17 findings
Members of withdrawal queue do not receive HIP3 fee
State
- Fixed
PR #52
Severity
- Severity: Low
Submitted by
rvierdiiev
Description
When fees are distributed using
stakeFees()function, the_exGhostLSTsupply is increased based on the stake amount, while newexLSTtokens are not minted. As a result, theexLST : _exGhostLSTrate decreases, and the Hype share of active stakers increases in value.When a withdrawal enters the blocking queue, some amount of
exLSTis burned and the corresponding_exGhostLSTshares are transferred to the queue. Once this happens, subsequentstakeFees()calls do not increase the Hype value for users in the withdrawal queue, because their_exGhostLSTbalance is fixed and does not grow. Therefore, queued members do not receive fees earned by the deployed market during the time they are waiting.Recommendation
As this is a known limitation, it is recommended to document the behavior clearly so users understand that fees accumulated while in the withdrawal queue will not be attributed to them.
Withdrawal inside blocking queue may be processed with updated fee share
State
- Acknowledged
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
rvierdiiev
Description
BlockedWithdrawalQueuecalculates unstaking fees at withdrawal processing time rather than when the withdrawal is queued. If the fee rate changes while the withdrawal is pending, the user will be charged using the updated unstaking fee rate. This may result in the user paying a different fee than expected at the time they initiated the withdrawal.Recommendation
Store the unstaking fee rate in the
BlockedWithdrawalstruct at the moment the withdrawal is queued, and use that stored value when calculating fees during withdrawal processing.Slippage protection is missing for the withdrawOnceLive
State
- Fixed
PR #59
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
rvierdiiev
Description
When a user withdraws with
withdrawOnceLive(), they intend to redeem a specificsharesamount ofexLST. However, depending onavailableWithdrawals()amount, part—or even all—of the withdrawal may become blocked and placed into theBlockedWithdrawalQueuefor an unknown period of time.Currently, there is no slippage protection mechanism. In the worst-case scenario, the entire withdrawal request can be forced into the blocked queue, which may be highly unexpected and undesirable for the user.
Recommendation
Introduce an additional parameter (e.g.,
maxBlockedShares) to define the maximum amount ofsharesthe user is willing to have queued. If more would be blocked, revert the transaction to protect the user from unintended outcomes.Tokens can't be rescued from HIP3StakingManager contract
State
- Acknowledged
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
rvierdiiev
Description
StakingManagerincludes arescueToken()function that sends accidentally stuck tokens to the treasury:function rescueToken(address token, uint256 amount) external onlyRole(TREASURY_ROLE) whenNotPaused { require(amount > 0, "Invalid amount"); // Prevent withdrawing HYPE & kHYPE tokens which are needed for the protocol require(token != address(kHYPE), "Cannot withdraw kHYPE"); // For ERC20 tokens - use safeTransfer instead of transfer IERC20(token).safeTransfer(treasury, amount); emit TokenRescued(token, amount, treasury);}For the
HIP3StakingManager, the treasury is theExManagercontract. However,ExManageritself does not implement any rescue mechanism, so tokens sent to theExManagercontract cannot be recovered.Recommendation
Implement a
rescueToken()function in theExManagercontract to allow the treasury to recover rescued tokens. Also make sure, it's not possible to withdrawexLSTandkHypetokens.Operator Bond Undercollateralized During Slashing Due to Share-Based Accounting
State
- Acknowledged
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
kamensec
Summary
Finding Description
The operator bond in
EXManageris tracked in shares (exLST) rather than underlying HYPE amounts. When bonding occurs viabond(), the operator deposits KHYPE which is converted to exLST shares at the current exchange rate. The bond requirement check on line 259 validates that minted shares meet theopBondminimum:// src/EXManager.sol:259if (bondShares < opBond) revert Errors.InsufficientBond();However, the HIP-3 self-bonding requirement and slashing mechanism operate on underlying HYPE amounts, not shares. If a slashing event occurs on the validator before or during the FUNDING phase, the HYPE:KHYPE exchange rate diverges from 1:1, meaning the operator's share-denominated bond represents less actual HYPE than intended.
The issue manifests in two scenarios:
-
Pre-bond slashing: If the validator experiences slashing before
bond()is called, the KHYPE deposited converts to fewer HYPE equivalent, but shares still meetopBondminimum. -
Post-bond slashing: After bonding, if slashing occurs, the operator's shares represent proportionally less HYPE, but their "skin in the game" relative to user deposits decreases since slashing affects the underlying proportionally.
Since slashing is denominated in HYPE (the underlying asset), the operator's effective exposure to slashing risk is reduced compared to regular users when the bond was established during a period of exchange rate deviation.
Impact Explanation
Impact is limited because:
- The protocol uses permissioned/trusted validators
- Slashing events are rare in practice
- The bond percentage (~10%) provides some buffer
Likelihood Explanation
Low likelihood because:
- Validator selection is permissioned and trusted
- Slashing events on HyperLiquid are rare
- The KHYPE:HYPE exchange rate is relatively stable during bonding phases
- Bonding typically occurs at protocol initialization when exchange rate is close to 1:1
However, the scenario is technically possible if:
- Slashing occurs on the linked validator before bonding
- The StakingManager/StakingAccountant used for KHYPE has experienced slashing
Recommendation (optional)
Consider tracking the operator bond in terms of underlying HYPE value rather than shares
Unbounded Chunk Size Can DoS Blocked Withdrawal Queue Processing
State
- Acknowledged
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
kamensec
Summary
Finding Description
The
processBlockedWithdrawals()function inBlockedWithdrawalQueueuses achunkSizeparameter to limit size of blocked withdrawals are processed per call. However, there is no upper bound validation on this parameter:if (withdrawableShares < Math.min(nextItem.remainingGhostShares, chunkSize)) { break; }The issue is on line 197 where the loop breaks if a single withdrawal's
withdrawableSharesis less than the minimum of either thenextItem.remainingGhostSharesorchunkSize.A whale user with a very large blocked withdrawal could:
- Queue a massive withdrawal that is larger than typical
availableHype - This withdrawal sits at the head of the blocked queue
- Every call to
processBlockedWithdrawals()hits the break condition immediately - Smaller withdrawals behind the whale are stuck indefinitely
The lack of an upper bound on
chunkSizeexacerbates this because:- Operator might set very large
chunkSizeexpecting to process many withdrawals - But if the first item is a whale, nothing gets processed regardless of
chunkSize - No mechanism to skip or partially process the blocking withdrawal
Impact Explanation
A single large withdrawal can block the entire withdrawal queue for other users.
The impact is constrained because;
- the operator can wait for sufficient HYPE to accumulate
- The chunk size can be reduced to unblock the DOS
Likelihood Explanation
Low likelihood because:
- Requires a user with holdings large enough to exceed typical available HYPE
- Protocol would need to set unrealistically large chunk size value
Recommendation
Add upper bound on chunk size
Compromised operator can bypass enforced whitelist to deposit in FUNDING phase
Description
Protocol implements an optional whitelist enforcement with tier-based user/global caps for deposits during the
FUNDINGphase. This logic is implemented in_enforceWhitelistMintCapIfEnabled(...)called fromdepositWhileFunding(...). However, to allow the operator to deposit its bond,_enforceWhitelistMintCapIfEnabled(...)returns successfully ifhasRole(OPERATOR_ROLE, sender).This operator exception for whitelisting assumes a trusted operator who does not deposit in the
FUNDINGphase. However, a compromised operator can exploit this exception to infinitely mint inFUNDINGphase thereby breaking the expected whitelisting enforcement. While the likelihood of a compromised operator is low in this permissioned setup, this trust assumption will be invalid in any future permissionless version.Recommendation
Consider refactoring logic to skip whitelist enforcement only during
UNBONDEDphase for operator bond payment instead of a permanent exception irrespective of the phase.Unenforced staking requirement of 500K HYPE may lead to unexpected behavior
State
- Acknowledged
Severity
- Severity: Low
Submitted by
0xRajeev
Description
Hyperliquid's HIP-3, which enables permissionless deployment of custom perpetual futures markets on the HyperCore layer, enforces a 500K HYPE staking requirement to serve as a bond for market deployers and deter any malicious behavior. This stake must be maintained at all times to operate a HIP-3 market. If the stake drops below 500K HYPE (e.g., due to attempted unstaking), the associated markets become inoperable forcing a wind-down, halting trading and other unexpected behavior.
To adhere to this staking requirement, the protocol enforces a minimum reserve requirement in the
LIVEphase usingglobalConfig.minHypeStake(). However, theminHypeStakeset inGlobalConfig.initialize()or in the setterGlobalConfig.setMinHypeStake(...)do not strictly enforce the 500K HYPE lower-bound requirement and instead assume that initialization orCONFIG_ADMIN_ROLEwill set this appropriately. This is supposedly to allow flexibility given that HIP-3 specifies that: "The staking requirement for mainnet will be 500K HYPE. This requirement is expected to decrease over time as the infrastructure matures."In the low likelihood that
minHypeStakeis set to a lower than 500K HYPE, this will cause the associated market to become inoperable leading to unexpected behavior for users including potential loss of fees/funds.Recommendation
Consider strictly enforcing the current HIP-3 500K HYPE staking requirement with the flexibility to change this in future contract upgrades.
Kinetiq
Acknowledged, but prefer to not upgrade given using similar security setup with global config contract roles as in the user able to upgrade contract.
Cantina
Acknowledged.
Missing check allows tokens to get stuck in EXRouter
Description
The withdraw functions in
EXManagerperformif (recipient == address(this)) revert Errors.InvalidRecipient()check on recipient to prevent tokens from accidentally getting stuck in the contract. However,EXRouter, which implements user convenience function wrappers is missing a similar check inwithdraw(...). This allows user tokens to accidentally get stuck inEXRouter.Recommendation
Consider implementing a
if (recipient == address(this)) revert Errors.InvalidRecipient()check inEXRouter.withdraw(...).Incorrect value emitted in Bonded event may affect offchain tracking
Description
ExManager.bond()emits aBondedevent to capture the amount of exLST shares escrowed for operator bonded capital. This event uses thesharesvalue instead ofopBond. However,sharesminted may be in excess ofopBondin which casebond()returns any excess exLST shares back to the operator. Given this, the correct value to be emitted isopBondand notshares. This incorrect value emitted inBondedevent may affect any offchain tracking of operator bonded capital.Recommendation
Consider emitting
opBondvalue instead ofshares.Missing upper bound check for minimumWithdrawWhenLive may prevent user withdrawals
State
- Acknowledged
Severity
- Severity: Low
Submitted by
0xRajeev
Description
Protocol implements a
minimumWithdrawWhenLive, which is the minimum amount of shares required for withdrawal when live. The motivating reason is to prevent small dust withdrawals that may supposedly overwhelm the system. ThisminimumWithdrawWhenLiveis set byMANAGER_ROLEusingsetMinimumWithdrawWhenLive(...)and enforced inwithdrawOnceLive(...)by reverting whenshares < minimumWithdrawWhenLive.However, the
setMinimumWithdrawWhenLive(...)setter is missing a reasonable upper bound check which allows the (compromised) manager to (intentionally) accidentally setminimumWithdrawWhenLiveto a large enough value to prevent all/many withdrawals.Recommendation
Consider implementing a a reasonable upper bound constant for
minimumWithdrawWhenLive, which can be checked insetMinimumWithdrawWhenLive(...).Kinetiq
Acknowledged. Will address in the fully permissionless version.
Cantina
Acknowledged.
WOUND_DOWN Phase Allows New Withdrawals to Bypass Users In Blocked Queue FIFO Ordering
Finding Description
The
BlockedWithdrawalQueueenforces FIFO (first-in-first-out) ordering during the LIVE phase to ensure fair withdrawal processing when liquidity is constrained. WhenavailableWithdrawals()returns 0 due to an existing blocked queue, all new withdrawals are forced to join the blocked queue:// src/EXManager.sol:859-863function availableWithdrawals() public view returns (uint256 availableShares) { if (exPhase == EXPhase.LIVE) { if (blockedWithdrawalQueue.totalBlockedQueue() > 0) { return 0; // Forces new withdrawals to join blocked queue } // ... calculate based on minHypeStake } else { return _totalSupply(); // WOUND_DOWN: ALL shares available }}However, when the protocol transitions to
WOUND_DOWNphase, this FIFO enforcement is bypassed. New withdrawals viawithdrawOnceLive()go directly to the StakingManager queue, while users already in the blocked queue must wait forprocessBlockedWithdrawals()to be called first.The blocked queue still processes in FIFO order internally:
// src/BlockedWithdrawalQueue.sol:191-193// Process blocked withdrawals in FIFO orderfor (; i < n; i++) { BlockedWithdrawal storage nextItem = blockedWithdrawals[i]; // ...}But new WOUND_DOWN withdrawals completely bypass this queue, allowing latecomers to jump ahead of users who have been waiting in the blocked queue.
Impact Explanation
Users who withdrew first and were placed in the blocked queue during LIVE phase can be jumped by users who withdraw after the WOUND_DOWN transition. Although intention is that block queue processing is allowed to complete in its entirety after unwind, the assumption is that both queues direct withdrawal and blocked queue can finalize immediately after WOUND_DOWN. However, there is a withdrawal limit on hyperliquid of 5 per address, and since each block is processed by the staking manager every 36 hours (a business limitation), so if more than 3 withdrawals are not finalized, its possible the direct withdrawal is placed in front within the 5 withdrawals, whilst the next withdrawal from the blocked queue is in the next batch. This violates the fairness that the blocked queue was designed to enforce forcing blocked withdrawals to be delayed by 36 hours (or block processing time).
Likelihood Explanation
Medium - requires the protocol to enter WOUND_DOWN phase while a blocked queue exists. The operator has a 7-day delay between
setUnwindPhase()andunwind(), during which the blocked queue could theoretically be processed. However, if liquidity remains constrained during this period, the blocked queue may persist into WOUND_DOWN and users are in fact benefited by delaying withdrawal until unwinding period is activated.Recommendation
Modify
availableWithdrawals()to respect the blocked queue in WOUND_DOWN phase, or add a mechanism to ensure blocked queue users are processed before new WOUND_DOWN withdrawals.L1 Account Activation Not Verified Before CoreWriter Actions
State
- Acknowledged
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
kamensec
Finding Description
HyperCore accounts must be activated before they can execute CoreWriter actions such as staking deposits. According to HyperLiquid documentation, activation occurs when an account receives its first spot transfer from an already-activated sender, which charges a 1 USDC activation fee.
The
StakingManager.stake()function calls_distributeStake()withOperationType.UserDeposit, which performs two L1 operations without verifying the StakingManager's L1 account is activated:// lib/lst/src/StakingManager.sol:249function stake() external payable nonReentrant whenNotPaused returns (uint256 kHYPEAmount) { // ... validation ... kHYPE.mint(msg.sender, kHYPEAmount); _distributeStake(msg.value, OperationType.UserDeposit); // No activation check stakingAccountant.recordStake(msg.value);}Within
_distributeStake()forUserDeposit:// lib/lst/src/StakingManager.sol:482-490// 1. Move HYPE from EVM to spot balance on L1(bool success,) = payable(L1_HYPE_CONTRACT).call{value: amount}("");require(success, "Failed to send HYPE to L1"); // 2. Move from spot balance to staking balance (CoreWriter action)uint256 truncatedAmount = _convertTo8Decimals(amount, false);L1Write.sendCDeposit(uint64(truncatedAmount)); // Requires activated account! // 3. Queue the delegation operation_queueL1Operation(validator, truncatedAmount, operationType);Then
L1_HYPE_CONTRACT.call{value: amount}is executed, this triggersCoreExecution.executeNativeTransfer()which has theinitAccountWithTokenmodifier. However, this modifier calls_initializeAccount()which checks if the account already exists on Core:// hyper-evm-lib/test/simulation/hyper-core/CoreState.sol:183-190RealL1Read.CoreUserExists memory coreUserExists = RealL1Read.coreUserExists(_account);if (!coreUserExists.exists && !force) { return; // Early return - does NOT activate!}_initializedAccounts[_account] = true;account.activated = true;If
coreUserExistsreturns false (account never received a spot transfer), the function returns early without activating. The HYPE then goes to latent balance instead of usable spot balance:// hyper-evm-lib/test/simulation/hyper-core/CoreExecution.sol:51-55if (_accounts[from].activated) { _accounts[from].spot[HYPE_TOKEN_INDEX] += (value / 1e10).toUint64();} else { _latentSpotBalance[from][HYPE_TOKEN_INDEX] += (value / 1e10).toUint64(); // Unusable!}The subsequent
L1Write.sendCDeposit()call attempts to move funds from spot balance to staking balance. CoreWriter actions from unactivated accounts fail silently - the EVM transaction succeeds but the L1 operation is not executed (guarded bywhenActivatedmodifier which returns early).Impact Explanation
If the StakingManager's L1 account is not activated before the first staking operation:
- Users call
stake()and receive kHYPE tokens (EVM state updated) - HYPE is sent to L1 but lands in latent balance (not usable spot balance)
sendCDeposit()silently fails (account not activated)sendTokenDelegate()silently fails (account not activated)- Result: Users hold kHYPE backed by HYPE that is stuck in latent balance and never actually staked
This creates unbacked LST shares and in some cases underlying reverts during market launch if minimum hype balances are insufficient.
Likelihood Explanation
Low - the deployment script provides an option to activate the account by sending USDC, and operators are expected to follow proper initialization procedures. However, this is an optional step with no on-chain enforcement, relying entirely on off-chain operational discipline.
Recommendation
Add the
coreUserExistsprecompile toL1Read.soland verify account activation before the first CoreWriter action.EXManager and BlockedWithdrawalQueue lack support of cancelled withdrawal requests
State
- Acknowledged
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: High Submitted by
Optimum
Description
StakingManagerallows users to queue withdrawal requests viaqueueWithdrawal(). This function transfers LST tokens from the caller and creates a corresponding withdrawal request object.
A privileged account withMANAGER_ROLEmay later cancel a withdrawal request by callingcancelWithdrawal(), which reverses the effects ofqueueWithdrawal()by deleting the request object and refunding the LST tokens to the original requester.However, both
EXManagerandBlockedWithdrawalQueuedo not implement any logic to handle cancelled withdrawal requests. As a result, if a withdrawal initiated by these contracts is cancelled, the refunded LST tokens will be sent back to them without any mechanism to correctly account for, forward, or recover these funds. This leads to a loss of funds or stuck tokens for users interacting through these contracts.While the impact is high due to the potential for permanent fund loss, the likelihood is assumed to be low because cancellations performed by the manager on requests originating from these contracts are not expected operationally.
Recommendation
Add explicit support in both
EXManagerandBlockedWithdrawalQueueto properly handle LST refunds triggered bycancelWithdrawal().Compounding precision loss in _HYPEToEXLST increasingly delays withdrawals as protocol yields grow
State
- Acknowledged
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
kamensec
Description
In EXManager.availableWithdrawals(), the calculation of available shares for immediate withdrawal undergoes two sequential rounding-down operations in _HYPEToEXLST():
// LSTPayments.sol:194-203 function _HYPEToEXLST( uint256 hypeAmount, IStakingAccountant reserveStakingAccountant, address reserveLST, uint256 exLstTotalSupply ) internal view returns (uint256 shares) { uint256 lstShares = _HYPEToLST(reserveStakingAccountant, hypeAmount); // Rounds down uint256 lstReserves = _reserves(reserveLST); shares = _calcShares(lstShares, lstReserves, exLstTotalSupply); // Rounds down }This causes availableWithdrawals() to return a value slightly lower than the theoretical maximum. As the protocol's exchange rate increases due to yield accrual, the HYPE value represented by each lost share grows, meaning users lose access to increasingly valuable immediate withdrawal capacity.
If the delta available hype is 100,000 (600k - 500k minimum), exchange rate is 100e18, so HYPEAMOUNT < 100 will round down. As the exchange rate grows this rounding becomes more pronounces
Recommendation
Acknowledge this behaviour, this can be limited with continuous reward claiming to reduce exchange rate growth having an impact on withdrawing the excess hype amounts above minimum.
initEXManager() might be front ran to disrupt deployment
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Optimum
Description
HIP3StakingManageris extendingStakingManagerand is meant to be used as an upgradeable contract under a proxy. In the current version of the code there are no deployment scripts but we were able to find the following function inHIP3StakingManagerTest:function _initHIP3StakingManager(address proxyAdmin, address proxy, address implementation) internal { uint256 minStakeAmount = 1 ether; uint256 maxStakeAmount = 10 ether; uint256 stakingLimit = 1000 ether; uint64 hypeTokenId = 1105; // Act - Initialize the contract vm.startPrank(admin); ProxyAdmin(proxyAdmin) .upgradeAndCall( ITransparentUpgradeableProxy(proxy), implementation, abi.encodeCall( StakingManager.initialize, ( admin, operator, manager, pauserRegistry, kHYPE, validatorManager, stakingAccountant, treasury, minStakeAmount, maxStakeAmount, stakingLimit, hypeTokenId ) ) ); HIP3StakingManager(payable(proxy)).initEXManager(exchangeManager); vm.stopPrank();}As we can see,
StakingManager.initialize()is followed by a call toinitEXManager(), although this is only a test script, a similar deployment scripts might be used, which will not ensure no transactions in between the two calls.Front runners might be able to call
initEXManager()right before the deployment script, and set theexManagerto their desired address. note that it will call the script call toinitEXManager()to revert which might be spotted by the deployers which will try to deploy again.Recommendation
Consider either restricting
initEXManager()to the manager or operator defined insideStakingManager.initialize()only, or alternatively, declare it as the maininitialize()function, which will callStakingManager.initialize().ExManager.confirmWithdrawal() can be front ran causing a revert for the original caller
Description
confirmWithdrawal()is permissionless and can be called by anyone for anyrecipient.
This design introduces a griefing vector: a frontrunner can pre-emptively confirm another user’s withdrawal.
While the attacker’s confirmation succeeds, the original user’s transaction attempting to confirm the same withdrawal will then revert.This issue also affects
EXRouter.confirmAll().
Since a single failing withdrawal confirmation causes the entire loop to revert, an attacker can repeatedly trigger failures and preventconfirmAll()from completing successfully.Recommendation
Modify
ExManager.confirmWithdrawal()to avoid reverting when the withdrawal is already confirmed or otherwise invalid.
Instead, follow a non-reverting pattern similar toBlockedWithdrawalQueue.confirmBlockedWithdrawal(), allowing the function to return gracefully.
This prevents the griefing vector and ensuresconfirmAll()can complete even when some confirmations are no-ops.
Informational8 findings
Missing zero-address checks
Description
While the protocol implements the best-practice of performing zero-address checks in most places, there are two missing checks for
_globalAdminand_configAdmin.Recommendation
Consider adding the missing zero-address checks.
Missing checks for user-provided addresses are risky
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
0xRajeev
Description
There are functions that accept user-provided addresses to later make external calls on them. While they do not seem to pose an immediate risk given the current context of the calls, avoiding arbitrary external calls by enforcing checks on user-provided addresses is a security best-practice.
Recommendation
Consider adding the missing checks for user-provided addresses.
Kinetiq
Acknowledged. In full permissionless version will have factory registry to check against in ex router.
Cantina
Acknowledged.
Missing upper bound check for unwindDelay may prevent markets from being wound down
Description
Protocol implements a
unwindDelayto enforce a delay from the time an operator callssetUnwindPhase(...)to when it can successfully callunwind()for winding down a market. The motivating reason is to allow a window of opportunity for users to react to the market being wound down.However, the
setUnwindDelay(...)setter is missing a reasonable upper bound check, which allows the (compromised)CONFIG_ADMIN_ROLEto (intentionally) accidentally setunwindDelayto a large enough value to prevent market wind downs.Recommendation
Consider implementing a a reasonable upper bound constant for
unwindDelay, which can be checked insetUnwindDelay(...).Compromised/Malfunctional privileged roles/addresses can cause unexpected behavior
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
0xRajeev
Description
The protocol has several privileged roles such as
MANAGER_ROLE,OPERATOR_ROLEandCONFIG_ADMIN_ROLEalong with other privileged addresses such aswhitelisterandexWalletAdmin, which are responsible for critical administrative/operational actions.Any compromised/malfunctional privileged role/address can cause unexpected behavior if it calls its authorized functions with incorrect values or in an incorrect manner accidentally/intentionally. While there are some safeguard checks to prevent such behavior, these nevertheless pose a centralization risk in this permissioned setup.
Recommendation
Consider:
- Implementing the highest levels of operational security for privileged role management.
- Documenting and highlighting the assumptions and risks for protocol users.
Potential temporary denial of service of withdrawals due to reverts from exTreasury
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Optimum
Description
Both
BlockedWithdrawalQueue.confirmBlockedWithdrawal()andExManager.confirmWithdrawal()transfers HYPE to two different addresses, the first is the recipient of the withdrawal and the second isexTreasurythat receives the fees.However, there is a potential scenario in which
exTreasurywill revert receiving the fees. In this case withdrawals will be blocked untilexTreasurywill be replaced.Recommendation
Consider changing the code so that fee payments won't revert the entire transaction.
Withdrawal of dust shares is not supported
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
0xRajeev
Description
withdrawOnceLive(...)reverts ifshares < minimumWithdrawWhenLive. WhileminimumWithdrawWhenLiveis expected to be zero during normal operations, it may be set to a non-zero value in the presence of a blocked withdrawal queue and small withdrawal spam. In such cases, the protocol expects dust shares to be redeemed via secondary market exits. However, that may not always be feasible.Recommendation
Consider if/when small withdrawals need to be prevented and if in-protocol mechanisms can be provided as an alternative.
Kinetiq
Acknowledged, but expect secondary market liquidity to be greater than dust.
Cantina
Acknowledged.
_exGhostLST should be initialized with the LST of exStakingManager
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Optimum
Description
During
initialize(),_exGhostLSTis set to the parameter ofexGhostLST_. Under the hood, this value should be equal toexStakingManager.KHYPEbut this is never verified, which opens the unnecessary possibility for a mistake during deployment.Recommendation
Consider adding the verification check that
exStakingManager.KHYPE == exGhostLST_.Missing Reentrancy Guards on State-Changing Functions
Description
Several functions across the codebase perform state updates, external calls, or token transfers without the use of a reentrancy guard. While some of these functions may not currently appear reentrant in their intended usage, the absence of explicit protection leaves the contracts more fragile and increases the risk that a future code change, integration, or unforeseen callback path could introduce a reentrancy vulnerability.
Recommendation
Add
nonReentrantmodifiers to functions that modify state, interact with external contracts, or transfer tokens, unless reentrancy is explicitly intended and safely handled.