Bounce engaged Guardian to review the security of their Automation Layer & Contract Updates. From the 27th of October to the 13th of November, a team of 3 auditors reviewed the source code in scope.
- Published
- Review window
- October 27 to November 13, 2025
- Rounds
- Main Review, Remediation Review
- Language
- Python, Solidity
- Chains
- Hyperliquid, Offchain
- Sector
- Derivatives
- 0 Critical
- 1 High
- 11 Medium
- 23 Low
- 18 Informational
Scope
Overview
Bounce engaged Guardian to review the security of their Automation Layer & Contract Updates. From the 27th of October to the 13th of November, a team of 3 auditors reviewed the source code in scope.
Findings 53
Main Review
32 findings-
H-01 High Incorrect Position Distribution Amount Logical Error Resolved
Description
The fund distribution contains balances in evm, spot, perps and position locations. This distribution is later used in different areas of the code, specially to calculate leveraged token TVL and target leverage.
However, the
hypercore-positiondistribution uses position size instead of the margin used.Consequently, following impact arise:
- Inflated TVL, as it will combine USDT/USDC balances in evm, spot and perps with the perp asset
position size in USDC terms, causing wrong comparison checks with the minimum TVL to stop exposure.
- Incorrect target leverage calculation, as the
position_margin_usdcwill be assigned to the notional
value of the position instead of the margin used, leading to a totally unexpected value.
- Objective type will not be calculated correctly, as the
hypercore-positionfund is inflated, so will
always try to send funds back to EVM to align with the smart contract buffer configurations.
Recommendation
The
hypercore-positiondistribution should use the position margin instead of position notional.Resolution
Bounce Team: The issue was resolved in PR#files.
-
M-01 Medium Adding Agent Wallets Will Silently Revert Logical Error Resolved
Description
When adding an API wallet on the
LeveragedTokencontract, the agent's name is obtained by concatenating the token's symbol, leverage, side, "AGENT" word and agent's slot.The initial token deployments will use BTC, ETH, HYPE and SOL. The agent's names for these tokens will have between 13-15 characters, depending on the token and leverage (i.e.
ETH5S_AGENT_2orHYPE10L_AGENT_1).There are tokens with longer symbols, like
FARTCOIN. With a 2 digit leverage, the agent's name will be longer:FARTCOIN10L_AGENT_1. However, agent's names can't exceed 16 characters, so any call tosetAgentfor certain tokens succeed in EVM but revert in Core.Recommendation
Consider reducing the agent's character length to support all tokens.
Resolution
Bounce Team: The issue was resolved in PR#141.
-
M-02 Medium Retroactive Streaming Fee Charge Logical Error Acknowledged
Description
Proof of concept: PoC
Owner can change streaming fee at any time using
GlobalStorage. In case the streaming fee is increased, the subsequentcheckpointcall invoked by any action computestimeElapsedsince the oldlastCheckpointand charges fees for the entire period during which the fee was lower, effectively retroactively charging fees at an increased rate.Recommendation
Make sure to call checkpoint in live leveraged tokens before increasing streaming fees.
Resolution
Bounce Team: Acknowledged.
-
M-03 Medium GKE Control Plane Exposed Access Control Resolved
Description
The Google Kubernetes Engine (GKE) cluster is configured to expose its control plane (the Kubernetes API server) to the entire public internet. The
master_authorized_networks_configis set to allow access from 0.0.0.0/0, which is a wildcard for all IP addresses.Risk:
Exposing the Kubernetes control plane to the public internet is a critical security risk and significantly increases the cluster's attack surface. 1. Increased Attack Surface: It makes the Kubernetes API server a direct target for attackers anywhere in the world. 2. Vulnerability to Attacks: The exposed control plane is vulnerable to a wide range of attacks, including brute-force password attempts, credential stuffing, and exploits targeting any potential vulnerabilities (including zero-days) in the Kubernetes API server. 3. Total Cluster Compromise: The control plane is the "brain" of the Kubernetes cluster. An attacker who successfully compromises the control plane can gain complete administrative control over the entire cluster. This would allow them to deploy malicious workloads, exfiltrate or destroy sensitive data, and use the cluster's resources for their own purposes.
Recommendation
Access to the GKE control plane should be restricted to the smallest possible set of trusted IP addresses.
- Enable and Configure Master Authorized Networks: The most immediate fix is to replace 0.0.0.0/0 with a specific, limited set of
IP address ranges. These should be the public IPs of trusted networks from which administrators or automation will access the cluster (e.g., corporate office networks, CI/CD runner IPs, or bastion host IPs).
- Example:
1 master_authorized_networks_config { 2 cidr_blocks { 3 cidr_block = "203.0.113.5/32" 4 display_name = "Corporate Office" 5 } 6 }
- Use a Private Cluster (Best Practice): For a much higher level of security, configure the GKE cluster as a Private Cluster. This
provisions the control plane with an internal IP address, making it inaccessible from the public internet. Access to the control plane would then be managed through a bastion host, a VPN, or IAP (Identity-Aware Proxy) tunneling within your Google Cloud VPC.
Resolution
Bounce Team: The issue was resolved in PR#66.
-
M-04 Medium Spot Assets Swaps Slippage Too High Configuration Resolved
Description
Both
UsdcSpotToAssetandSpotAssetToUsdcclasses are initialized with a slippage value fetched from the env configs.However, the mainnet json config file sets 2% for the
spot_conversion_slippage. Therefore, any spot swaps (USDC <=> baseAsset) will allow 2% slippage, which is too high for stablecoin swaps.Recommendation
Consider lowering the slippage for spot market orders.
Resolution
Bounce Team: The issue was resolved in PR#61.
-
M-05 Medium Unsafe Deserialization Logical Error Resolved
Description
The
from_configclassmethod is responsible for deserializing a leveraged token from either a dictionary or a string. It contains two weaknesses that can lead to application errors or the processing of invalid data.Unsafe Dictionary Deserialization: When parsing a dictionary, the code accesses keys directly (e.g.,
value['leverage']). If a provided dictionary is missing any of the required keys (perp_asset, leverage,is_long), the application will raise an unhandledKeyErrorand crash.Overly Permissive String Parsing: The regular expression used to parse string inputs (^([A-Za-z]+)...) allows any sequence of one or more letters for the asset name. This is too broad and does not enforce a standard ticker format (e.g., 2-6 uppercase letters).
It could allow the creation of invalid keys like "
MyInvalidAsset10L", which may cause silent failures or errors in downstream systems that expect a standard asset ticker.Recommendation
The deserialization logic should be hardened to be more resilient to malformed input.
For Dictionary Input: Use the
.get()method to safely access keys. Check if any required values are missing and raise a single, clearValueErrorthat lists the missing fields.For String Input: Tighten the parsing to ensure that the leverage is not extracted from given string through regex,
Resolution
Bounce Team: The issue was resolved in PR#91.
-
M-06 Medium Temporary Redemption Execution Loop Logical Error Resolved
Description
The redemption service is in charge of checking pending redemptions and executing them in order of appearance.
However, there could be cases where a
PREPARE_REDEEMevent is processed, but while theREMOVE_MARGINevent flow is executed, the objective steps either fail or they are skipped due to specified rules.Therefore, a pending redemption will be created with insufficient evm contract funds to execute the redemption. The following scenario might take place:
- User prepares redemption
- Perp limit order fails due to slippage, next steps are skipped do to min transfer amounts (rules).
- Executor sees pending redemption, tries to execute it
- As there is not enough balance in contract, it skips user and the for loop ends
- As tx does not revert, automation thinks redemption was processed
- One iteration every 60 sec, which fetches pending redemptions again, and process repeats.
- Executor will keep trying to execute redemptions, spending HYPE in each tx.
- Loop will continue until a new margin change event occurs, to recalculate distribution and steps.
Keep in mind this scenario might also appear if a user prepares redeem when there is no active position (i.e. funds were bridged to EVM but limit order failed), as the rebalance service will not trigger a margin change event if position is empty.
Recommendation
Either add a retry mechanism so funds are bridged back to EVM, or avoid calling
executeRedemptionsmore than once with same params, until a new margin changing event occursResolution
Bounce Team: The issue was resolved in PR#48.
-
M-07 Medium Sandwich Attack On Large Position Updates MEV Acknowledged
Description
When executing a perps market order, scripts will calculate limit price and slippage, but no validation is done for the notional value.
In case there is a large mint or redeem, position increase/decrease value might be large, causing a significant price jump in the order book.
Although there is a limit price calculation and slippage protection, attackers can back run the EVM transaction and front run the hyperliquid order, and profit from the price jump.
Recommendation
Consider using TWAP orders if order notional value is above certain threshold.
Resolution
Bounce Team: Acknowledged.
-
M-08 Medium Minimum Margin Ratio Configuration Resolved
Description
The current environment configuration uses
minimum_margin_ratio=0.15andposition_to_open.This suggests that the scripts will try to open a 9x leverage initial position, but then validates that the margin used is above 15% of the projected notional.
However, a 9x leverage position yields to a 11,11% margin ratio, which will always be higher than the minimum configured margin ratio, disallowing position opening due to
MinimumMarginPositionRuleRecommendation
Consider reducing the
minimum_margin_ratioto a value below 11,11% to allow positions to be opened.Resolution
Bounce Team: The issue was resolved in PR#47.
-
M-09 Medium No Delay Between Bridge Funds Steps Unexpected Behavior Resolved
Description
During
PositionServiceexecution of multi-step fund transfers (e.g.,ContractToSpot), there is no delay or block confirmation wait after an EVM bridge transaction.As a result it could happen, that the next step could fail because the balance change is not immediately reflected there, resulting in skipped steps, stuck funds.
Recommendation
Add some delay after the EVM to Core bridge tx executed to make sure all previous tx are processed and finalized.
Resolution
Bounce Team: The issue was resolved in PR#68.
-
M-10 Medium Partial TVL Calculation In Case Of Failures Logical Error Resolved
Description
The application's Total Value Locked (TVL) calculation process, specifically within the
_get_current_distributionmethod, is designed to be resilient to individual data source failures.When attempting to fetch fund balances from various FundLocations (e.g., blockchain contracts, Hyperliquid exchange accounts), any exception that occurs during an API or RPC call for a specific location is caught.
Instead of halting the entire TVL calculation, the system logs a warning and proceeds to the next
FundLocation, effectively excluding the funds from the failed source.src/services/position_service.py(400-410,_get_current_distributionmethod)
Recommendation
- Evaluate Acceptable Risk: Determine if a partial TVL calculation is an acceptable operational
state. For critical financial decisions, it might be preferable for the entire TVL calculation to fail if a significant data source is unavailable.
- Enhanced Alerting: If a partial TVL is deemed acceptable, implement more prominent logging and
alerting mechanisms (e.g.,
PagerDuty, Slack notification) when a data source fails to fetch balances. This ensures operators are immediately aware of the incomplete data.- Graceful Degradation Strategy: If a data source is critical, consider a strategy where the system
either:
- Uses the last known good balance for a short period.
- Temporarily pauses operations that rely on the TVL until all critical data sources are restored.
- Clearer Reporting: Ensure that any UI or reporting of the TVL clearly indicates if it is based on a
partial dataset due to data source failures.
Resolution
Bounce Team: The issue was resolved in PR#92.
-
L-01 Low LT Redeployment Could Lead To Unprocessed Events Unexpected Behavior Resolved
Description
The automation layer always queries the latest list of the leverage tokens by calling the factory state.
In case an LT gets redeployed (only condition to not have any
marginUsedfor it) the given address gets removed from the factory lts list.This could cause issues in case there is a pending redeem or other kind of events in the automation side for the old lt token instance.
Recommendation
In the factory's
redeployLtfunction you could check if there are any pending redemption in the given lt instance, or somehow on automation side cache and gracefully process all the pending events for an lt token before start processing the new ones.Resolution
Bounce Team: The issue was resolved in PR#70.
-
L-02 Low Unencrypted Redis Connection Configuration Resolved
Description
The application communicates with the Redis server over a plain TCP connection, which is unencrypted. All data exchanged between the application and Redis is transmitted in plaintext.
Recommendation
- Configure the Redis server to support and require SSL/TLS connections.
- Update the application's connection logic in
src/storage/redis_storage.pyto include the necessary
SSL parameters (e.g.,
ssl=True,ssl_ca_certs) to establish a secure, encrypted connection.Resolution
Bounce Team: The issue was resolved in PR#58.
-
L-03 Low Unauthenticated Redis Connection Access Control Resolved
Description
The application connects to the Redis server without providing a password or any other form of authentication.
Risk:
This allows any user or process with network access to the Redis server to gain full control over the data it holds. An unauthorized actor could read, modify, or delete data, disrupt application logic (like leader election), or execute administrative commands on the Redis server.
Evidence: 1. Explicit Log Message: The code contains a log message that explicitly states the connection is unauthenticated.
- File:
src/storage/storage_type.py - Lines: 35-36
- Code Snippet:
1 // ... 2 34 logger.info( 3 35 f'Multi-process mode: Connecting to Redis without authentication at {redis_host}:{redis_port_int}/{redis_db_int}') 4 36 5 37 storage = RedisStorage( 6 // ... 1. Redis Client Instantiation: The redis.Redis client is instantiated without the password parameter, confirming that no credentials are being used.
- File:
src/storage/redis_storage.py - Lines: 18 and 21 (same as the unencrypted finding)
- Code Snippet:
// ... redis_kwargs = { 'host': host, 'port': port, 'db': db, 'socket_connect_timeout': socket_connect_timeout, 'socket_timeout': None } self._redis_blocking = redis.Redis(**redis_kwargs) # No 'password' parameter ``` // ... self._redis = redis.Redis(**redis_kwargs) # No 'password' parameter // ...Recommendation
- Enable Redis Authentication: Configure the Redis server to require a password by setting the requirepass directive.
- Use Secrets Management: Store the Redis password securely in your secrets management system.
- Update Application Code: Modify the application to retrieve the Redis password and provide it in the
redis.Redisconstructor call using the
password parameter.
Resolution
Bounce Team: The issue was resolved in PR#58.
- File:
-
L-04 Low Use Of Mutable Tag For Base Image Best Practices Resolved
Description
The Dockerfile at deployments/Dockerfile uses python:3.11-slim as the base image for the container. This is a "floating" tag, meaning that the underlying image it points to can be updated over time by the image Maintainers.
Risk:
Using mutable tags like latest or 3.11-slim introduces several risks:
- Unpredictable Builds: Your CI/CD pipeline might pull a newer version of the base image without your knowledge. This new
version could contain breaking changes that cause your application to fail at build time or, worse, at runtime.
- Vulnerability Management: It becomes difficult to track and manage vulnerabilities. A new version of the base image could
introduce new security vulnerabilities (CVEs). Because your build is not pinned to a specific version, you can't be certain which version of the base image a particular build of your application is using, making vulnerability remediation challenging.
- Inconsistent Deployments: Different environments (e.g., development, staging, production) could end up running on different
underlying base images, even if they are all built from the same Dockerfile. This violates the principle of immutable infrastructure and can lead to "it works on my machine" problems.
Recommendation
Pin your base images to a specific, immutable version using a digest (@sha256:...). This guarantees that your builds are always reproducible and that you are using a known, vetted version of the base image.
- Find the Digest: First, pull the image you want to use and find its digest:
1 docker pull python:3.11-slim 2 docker inspect python:3.11-slim | grep "Digest" This will give you a sha256 digest. 1. Update the Dockerfile: Use the full image name, tag, and digest in your FROM instruction.
- Example:
1 FROM python:3.11-slim@sha256:a14e84333778030615168a46595f40e4d33e3519d18a7070a87972618543485c 2 3 WORKDIR /app 4 …
By pinning to the digest, you ensure that every build of your application will use the exact same base image, making your builds more secure and reliable.
Resolution
Bounce Team: The issue was resolved in PR#81.
-
L-05 Low Insufficient RPC URL Validation Validation Resolved
Description
The validation logic for RPC URLs is critically insufficient. It only checks if a URL string begins with the https:// prefix and fails to perform any validation on the URL's hostname. This allows URLs pointing to internal or restricted network locations to be accepted and used by the application.
This flaw creates a risk for Server-Side Request Forgery (SSRF) vulnerability. An attacker who can control the value of the RPC URL configuration (e.g., via an environment variable or a compromised secret store) can force the application to send requests to arbitrary network endpoints from the server's perspective.
This can be exploited to:
- Scan Internal Networks: Discover live hosts and open ports on the internal network that are not exposed to the
internet.
- Access Internal Services: Interact with internal databases, admin panels, or other services that may not require
authentication because they are assumed to be unreachable externally.
- Steal Cloud Credentials: Make requests to the cloud provider's metadata service (e.g., 169.254.169.254) to exfiltrate
temporary credentials, which can then be used to compromise other cloud resources.
Recommendation
The RPC URL validation logic must be enhanced to perform strict hostname validation. 1. Implement Strict Host Validation: The validation function should parse the URL to extract its hostname. 2. Resolve and Verify IP Address: The hostname should be resolved to its IP address. The function must then verify that the IP address is a public, non-reserved address. 3. Reject Restricted IPs: The validation must explicitly reject any URL whose hostname resolves to:
- Loopback addresses (127.0.0.0/8)
- Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
- Link-local addresses (169.254.0.0/16)
- Apply Centrally: This robust validation should be implemented in a single, centralized function (as recommended in
the "Duplicated Logic" finding) and used by all parts of the application that process RPC URLs.
Resolution
Bounce Team: The issue was resolved in PR#93.
-
L-06 Low No Slippage Protection MEV Acknowledged
Description
The
prepareRedeem→executeRedemptionsredemption path allows users to request redemptions asynchronously via executors, but provides no slippage protection for the user during execution.- In
prepareRedeem, the user locks their LT tokens and receives credit based on the current exchange
rate.
- Later, in
executeRedemptions, the executor processes the redemption using the exchange rate at
execution time, which may have moved adversely (especially for large redemptions or volatile markets).
- There is no
minBaseAmountparameter inprepareRedeem, and no check inexecuteRedemptionsto
ensure the user receives at least a minimum acceptable base asset amount.
- A whale redeeming a large position could suffer significant slippage if market conditions
deteriorate between
prepareRedeemand execution.This violates the principle of user-controlled slippage tolerance and could cause loss of funds for the user.
Recommendation
Add a
minBaseAmountparameter toprepareRedeem- Store this value per user in storage.
In
executeRedemptions, check slippage for the user Allow users to update their slippage or cancel redemption Document that large redemptions should use directredeem()for tight slippage control, or accept delayed execution risk.Resolution
Bounce Team: Acknowledged.
- In
-
L-07 Low Maximum Token Leverage Not Enforced Logical Error Resolved
Description
The
Factoryis in charge of deploying new leveraged tokens. It uses_validateLeverageto check if the target token leverage is within 1x and max leverage supported in Hyperliquid for given perp asset.The automation layer will not support tokens above 9x leverage, due to the
position_to_openconfig value.Furthermore, as the target leverage will normally be above the token specified leverage, if a token is deployed with a leverage close to 9x, it won't be supported as well, due to the
smart_contract_bufferof 20% (i.e. 8x / 0.8 = 10x > 9x).If any unsupported token leverage is deployed and added to the automation script management, there is no validation on the token leverage. If a mint event occurs on this leveraged token, funds will be bridged to core, but position will never be opened due to leverage rules.
Recommendation
If
smart_contract_buffer=0.2andposition_to_open=9, the max token leverage supported is 7.2x. Consider adding this check to the factory smart contract directly.Alternatively, consider validating the token leverage in the automation scripts, when reading
tokens_supportedfrom the config files.Resolution
Bounce Team: The issue was resolved in PR#73.
-
L-08 Low No Validation Across Config Files Validation Resolved
Description
The system uses multiple JSON config files (e.g., for different environments, services, or instances) that define
TOKENS_SUPPORTEDlists.However, there is no enforcement that these lists match across files/services, and no validation prevents duplicate tokens within a single list.
Could lead to double event processing. Token mismatch, missing event processing.
Recommendation
Create a single, centralized
supported_tokens.jsonfile containing all theTOKENS_SUPPORTEDlist. + Add Duplicate Check when reading the list of supported tokens.Resolution
Bounce Team: The issue was resolved in PR#75.
-
L-09 Low Application Fails To Start Due To Config Errors Configuration Resolved
Description
The application fails to launch in the single-process local development mode (python src/main.py). The root cause is a series of configuration loading and validation issues stemming from the fact that the architecture is primarily designed for a multi-process production environment.
The system incorrectly attempts to find and validate service-specific configuration files instead of gracefully sharing the main environment config (
config-testnet.json), leading to three distinct, sequential crashes on startup.Issue #1: Incorrect Config File Path Symptom: The application crashes immediately with
ValueError: Config fileconfig-testnet-main-main.jsoncould not be found. Root Cause: TheBaseFramework._create_instance_config_managermethod incorrectly constructs a file path intended for a multi-process environment. It combines the network, the service's config name (e.g.,position-service), and the instance name (main), resulting in a path to a file that does not exist in a local setup. It should instead load the mainconfig-testnet.jsonfile.Issue #2: Strict Schema Validation Failure Symptom: After fixing the file path, the application crashes during service initialization with
ValueError: Mandatory fieldmark_prices_supportedis missing from config. Root Cause: Although all services now correctly loadconfig-testnet.json, each service validates this shared file against its own unique schema which expects service-specific fields (e.g.,mark_prices_supportedforPriceCacheService). Since these fields are not in the generic environment config, the mandatory field check inConfigSnapshot.__post_init__fails.Issue #3: Unhandled None Values in Service Logic Symptom: After relaxing the schema validation, the application crashes in a service's validation callback (
PositionService.on_config_validation) withTypeError: 'NoneType' object is not iterable. Root Cause: By making the schema fields optional for local runs, the config manager now correctly returns None for missing fields. However, the downstream code within the services does not account for this None value and attempts to iterate over it directly, causing aTypeError.Recommendation
Correct Config Loading for Local Mode: In
src/framework/base_framework.py, the_create_instance_config_managerwas modified to detect the single-process mode (instance_name == 'main'). In this mode, it now:Correctly loads the main environment config file (e.g.,
config-testnet.json).Dynamically creates a new
ConfigSchemawith an emptymandatory_fieldsset. This allows the configuration to be loaded without failing validation on missing service-specific fields.Improve Service Robustness: In
src/services/position_service.py, theon_config_validationmethod was updated to handle None values returned from the config. It now provides default empty iterables (e.g., ... or [], ... or {}) to preventTypeErrorwhen a configuration field is not present.Improve Error Messaging: The error message in
_create_config_managerwas improved to clearly state which file it was looking for and where it searched, making future debugging easier.These changes ensure the application can adapt its configuration strategy for the local development environment, leading to a successful startup.
Resolution
Bounce Team: The issue was resolved in PR#76.
-
L-10 Low Service Related Settings Not Updated On Change Unexpected Behavior Resolved
Description
Each service (e.g.,
PositionService,RebalanceCheckService) has its ownconfigs.jsonfile but currently only theconfig-{network}.jsonbeing watched by the config manager.For example if a supported token gets removed for a service, the change won't be detected by the config manager.
Recommendation
Start the config manager for each service on startup.
Resolution
Bounce Team: The issue was resolved in PR#76.
-
I-01 Informational Errors Not Used Superfluous Code Resolved
Description
The following errors in
ILeveragedTokenare declared but never used:InsufficientCreditLastAgentAgentAlreadyUsed
Recommendation
Remove unused errors
Resolution
Bounce Team: The issue was resolved in PR#142.
-
I-02 Informational Temporary DoS In Leveraged Token Actions DoS Acknowledged
Description
Previously, a protocol deadlock could appear if a whale redeemed 100% of the base assets in the contract, as there will be no funds to pay streaming fee in
_checkpoint.However, users will still face a temporary DoS, as leaving contract without any base assets is still possible. Any transaction involving
mintorprepareRedeemwill still revert until the agent wallets bridge in funds fromHyperCore.Recommendation
As there is no trivial solution that does not involve refactoring a big part of the code, consider documenting this to users, so they are aware of these temporal DoS.
Resolution
Bounce Team: Acknowledged.
-
I-03 Informational Only Last Locker Registered Configuration Resolved
Description
The deployment script deploys three distinct
Lockercontracts (A, B, C) but callsglobalStorage.setLocker()for each, overwriting the prior address. As a result, only the last (Locker C) remains discoverable viaGlobalStorage, while Locker A and B are not registered.Any on-chain or off-chain components that rely on
GlobalStorageto reference all lockers will miss A and B, potentially breaking claims/operations for those allocations and orphaning distributed tokens.Recommendation
Provide dedicated setters in
GlobalStoragefor multiple lockers (e.g.,setLockerA/B/Cor a setter that stores an array). Update the script to call the correct distinct setters. If a single locker is intended, deploy only one and distribute accordingly.Resolution
Bounce Team: The issue was resolved in PR#143.
-
I-04 Informational Add Account Script Logs Private Key To Console Best Practices Resolved
Description
The generated accounts private key being logged as plain text to the console, which could be retrieved if the system gets compromised.
Recommendation
Remove the logging part of the pk.
Resolution
Bounce Team: The issue was resolved in PR#71.
-
I-05 Informational Potential Private Key Exposure In Shell Script Best Practices Resolved
Description
The shell script located at
scripts/internal/upload-account-to-secrets.shis designed to upload a private key to Google Secret Manager. It accepts the private key directly as a command-line argument.Risk:
Passing secrets as command-line arguments is a highly insecure practice. It can lead to the private key being exposed in several ways:
- Shell History: Most shells are configured to save a history of executed commands. The command, including the
private key, will be stored in plaintext in the user's shell history file (e.g., .
bash_history).- Process List: While the script is running, the full command, including the private key, can be viewed by any user on
the system who can list running processes (e.g., by using the ps aux command).
- Logs: If the command is executed as part of an automated script or CI/CD pipeline, it is likely to be logged, again
exposing the private key in plaintext.
An attacker who gains access to the user's account, the system's process list, or the relevant logs could easily retrieve the private key.
Recommendation
Modify the script to avoid passing the private key as a command-line argument. Instead, prompt the user to enter the private key interactively or read it from a file.
Example (Interactive Prompt):
1 # ... 2 read -sp "Enter private key: " PRIVATE_KEY 3 echo 4 # ... 5 # Add secret version with private key 6 echo -n "${PRIVATE_KEY}" | gcloud secrets versions add "${SECRET_ID}" --data-file=- …
Using read -s prevents the typed key from being displayed on the screen and from being stored in the shell history. This significantly reduces the risk of accidental exposure.
Resolution
Bounce Team: The issue was resolved in PR#77.
-
I-06 Informational Incorrect UsdcSpotToAsset Class Name Informational Resolved
Description
The
UsdcSpotToAssetclass appears to be a direct copy ofSpotAssetToUsdc— itsname()method still returns the old class name.Recommendation
Name function should return
UsdcSpotToAssetinstead.Resolution
Bounce Team: The issue was resolved in PR#65.
-
I-07 Informational Duplicated RPC URL Validation Logic Best Practices Resolved
Description
The logic responsible for parsing a comma-separated string of RPC URLs and performing basic validation is duplicated in at least three separate locations within the codebase. This indicates a lack of a centralized utility for handling this common data-processing task.
The primary impact of this issue is on code quality, maintainability, and operational risk:
- Increased Maintenance Overhead: When the validation logic needs to be updated—whether for a
bug fix or a feature enhancement—developers must find and modify every instance of the duplicated code. This is inefficient and error-prone.
- Risk of Inconsistent Behavior: It is easy for a developer to update the logic in one location but miss
the others. This can lead to different parts of the application behaving inconsistently, making debugging difficult.
- Compounded Security Risk: If a security flaw exists in the duplicated logic (as is the case here), the
risk is compounded because a patch must be applied correctly to all locations. Missing even one location leaves the system vulnerable.
Recommendation
- Centralize the Logic: Create a single, well-defined function (e.g.,
parse_and_validate_rpc_urls) in a
shared location, such as
src/common/utils.py.- Refactor for Reuse: Refactor the code in the identified locations to import and use this new,
centralized function, ensuring that all RPC URL processing is handled consistently across the application.
Resolution
Bounce Team: The issue was resolved in PR#64.
-
I-08 Informational No Fallback API For Price Updates Suggestion Acknowledged
Description
The
HyperLiquidSubscriberrelies exclusively on the HyperliquidWebSocketAPI (Info client) for real-time price updates (mark price, mid price, etc.). While it includes stale detection and automatic reconnection, there is no fallback data source if:- The Hyperliquid
WebSocketis down for an extended period - The API endpoint is rate-limited or blocked
- Network partitions prevent reconnection
This could result in stale price data. Even with reconnection logic, price updates cease entirely during outages — there is no secondary oracle or REST polling fallback
Recommendation
- Add a fallback REST polling mechanism using Hyperliquid’s HTTP API
- Activate fallback when stale threshold exceeded AND reconnection fails > N times
Or optionally use different system oracles not hyperliquid related
Resolution
Bounce Team: Acknowledged.
- The Hyperliquid
-
I-09 Informational Batching Events Could Save Gas Informational Acknowledged
Description
The
PositionServiceprocesses mint and redeem events independently viaEventProcessorwith per-token queues (queue_identifierbased onLeveragedTokenKey). This means:- A mint of 1000 USDC → triggers full fund transfer + position adjustment
- A redeem of 1000 USDC shortly after → triggers another full transfer in the opposite direction
Even though the net effect is zero, both events are fully processed, resulting in:
- Unnecessary gas fees (EVM contract calls)
- Unnecessary Hyperliquid orders (spot/perp trades)
- Increased slippage risk
There is no mechanism to:
- Batch opposing events (mint + redeem)
- Cancel or net out pending events before execution
- Skip redundant processing when net TVL change is below threshold
This is especially costly in high-frequency mint/redeem scenarios (e.g. arbitrage, rebalancing).
Recommendation
Add event batching & netting in EventProcessor Batch events per block or time window
Resolution
Bounce Team: Acknowledged.
-
I-10 Informational Margin Event Uses Wrong Objective Superfluous Code Acknowledged
Description
Proof of concept: PoC
Currently,
ADD/REMOVE_MARGINevents for existing positions emit a margin event withis_leverage_change = False.This results in using the
KEEP_SMART_CONTRACT_FUND_WITHIN_BUFFER objectiveType, which triggers fullAllocationPlannerruns and causes unnecessary API calls, calculations, and processing.Rebalancing may still be needed in some cases, but not always — only when the added or removed margin causes the position to move outside the buffer. In those cases, the timed rebalance checker can handle the adjustment separately.
Recommendation
Use
ADJUST_POSITION_ONLYwhen position already exists andis_leverage_change=false.Resolution
Bounce Team: Acknowledged.
-
I-11 Informational Regex Fails On Alphanumeric Tokens Logical Error Resolved
Description
The leveraged token key should have a specific format to guarantee a match here:
match = re.match(r'^([A-Za-z]+)([1-9]\d*)([LlSs])\$', value).Although this pattern will match currently supported tokens, it won't handle some edge cases, specially with alphanumeric tokens:
- if first character of the token name is a number, regex will not find a match and reverts (i.e.
0G 4) - if the token has a number at the end of the name (i.e.
SPX6900) and we want to short it 3x, which
would be
SPX69003S, the leverage value is extracted from the 2nd match group (matching numbers) in the regex pattern, which will lead to a 69003x leverage value.Recommendation
Document this issue in the code, explaining why certain tokens are not supported.
Resolution
Bounce Team: The issue was resolved in PR#72.
- if first character of the token name is a number, regex will not find a match and reverts (i.e.
Remediation Review
21 findings-
M-01 Medium Inability To Remove Activated Agent Wallets DoS Resolved
Description
The protocol owner is able to add agent wallets to the
LeveragedTokensusingsetAgent. This function now verifies if the contract address is already activated and if the agent wallet is NOT activated, to prevent silent reverts in Hyperliquid Core transaction.Although the agent wallet activation check was added as an additional precaution, it adds a new DoS attack, as this agent can't be removed if the agent is activated in Core.
Therefore, if the agent wallet is compromised or the wallet needs to be replaced, anyone can just activate it and trigger a revert in the
setAgentEVM transaction.Recommendation
Remove the
HyperCoreactivation check for the agent wallet, and instead perform this check off chain before adding new agents.Resolution
Bounce Team: The issue was resolved in PR#152.
-
L-01 Low Global Storage Owner Risk Best Practices Acknowledged
Description
The
GlobalStorageowner is set tomsg.senderduring deployment. Besides setting protocol config params, this address will also be allowed to perform critical changes to the bounce token, leveraged tokens, among others.However, the
msg.senderwill be the foundry scripts deployer address, an EOA set by thePRIVATE_KEYin env config.Deployment scripts do not include an ownership transfer to a more secure owner, like a multisig. This increases the risk of a complete protocol hijack if the keys are compromised.
Recommendation
Consider transferring ownership of the
GlobalStoragecontract to a multisig address in deployment scripts.Alternatively, add an address param to the
GlobalStorageconstructor, and use it to initialize theOwnableparent contract.Resolution
Bounce Team: Acknowledged.
-
L-02 Low Missing Token Support Validation Validation Partially resolved
Description
Although the
filter_tokens_by_leverage_constraintsfunction filters the unsupported leverage tokens, there are still some missing validation:- No validation at config time; config validation passes, service starts, only at runtime the tokens are
filtered
- Event queue mismatch; although
leverage_token_map.getfetches the filtered list, the
_enqueue_eventusesconfigs.token_supported.get_field.- Hyperliquid max leverage changes: If Hyperliquid reduces max leverage for an asset (e.g., HYPE
goes from 10x to 8x), the system won't detect this until the service restarts or a config reload triggers.
Recommendation
Consider adding the supported token validation on leverage constraints whenever the script reads token list.
Resolution
Bounce Team: The issue was resolved in PR#96.
-
L-03 Low Incorrect Redemption Execution Check Unexpected Behavior Resolved
Description
During the redemption execution flow, the scripts first check if the
executeRedemptionscall will revert with the user array length.The
executeRedemptions(users_checksum).callis the command used to check the transaction result, but it does not set a specific gas limit, so the node will use its own default value.This leads to an invalid execution checks, as the check passes simulation but the estimated gas for the transaction can exceed the Hyperliquid fast block gas limit.
Although the flow uses
_execute_with_halving_retryto reduce the user list and retry the transaction, it’s something that could have been avoided in the previous checks (binary search)/.During internal testing, if the list contains more than 27 executable users, it will exceed 3M gas.
Recommendation
Consider simulating the
executeRedemptionscall with agasvalue equal to the fast block gas limit:result = contract.functions.executeRedemptions(users_checksum).call({"from": self._account.address, "gas": 3_000_000})Resolution
Bounce Team: The issue was resolved in PR#95.
-
L-04 Low No Core Settlement Verification Logical Error Resolved
Description
The automation treats the EVM
bridgeIntransaction as final. After it confirms, a Hyperliquid Core spot transfer is triggered, but the scripts do not verify Core settlement.If the Core leg fails post-EVM (e.g., Core-side error or partial execution), the flow reports success and will not retry, leaving funds potentially stranded or state out of sync.
This issue may rise if a
bridgeIntransaction is send in EVM, but there are insufficient funds in Core to send.Recommendation
After EVM
bridgeInconfirmation, add a post-check against Hyperliquid (e.g., verify spot balance/transfer status) and raise/retry if the Core transfer did not settle.Alternatively, emit/log Core settlement status from the handler and require the automation to confirm it before marking success.
Resolution
Bounce Team: The issue was resolved in PR#107.
-
L-05 Low Incomplete Private Key Cleanup Mechanism Censoring Resolved
Description
The "
scripts/add-account.sh" script usestrap ... RETURN.for cleanup of the private key temporary files. However, this trap only executes if the function finishes its execution flow normally.If the script is interrupted (e.g., Ctrl+C), killed by the CI/CD runner (timeout/cancellation), or encounters a syntax error causing a crash, the RETURN signal is never sent.
As a result, the temporary file containing the plaintext private key will remain in /tmp/ (or the temp directory) indefinitely, accessible to anyone with sufficient permissions on that machine.
Recommendation
Use
trap ... EXIT(which covers script termination) or specifically trap signals like SIGINT and SIGTERM.Resolution
Bounce Team: The issue was resolved in PR#97.
-
L-06 Low Potential Exposure Of Private Key Through Logs Best Practices Resolved
Description
The line echo -n
"$private_key" > "$temp_key_file"is insecure in CI/CD and debugging environments.a Guardian proof of concept
3641c75cd5be639578f6564d6a78f#diff-65d3043d6a761a69290145f45d21970cf2ac6d4d0dd04fc0 69c713bf36e12328R115
If set -x (debug mode) is enabled anywhere in the script execution chain, the shell will expand the variable before running the command. The logs will literally show:
+ echo -n '0x3a1f...' >/tmp/tmp.XyZ.Thus, the private key is permanently recorded in the build logs, which are often viewable by a wider audience than the secrets manager itself.
Recommendation
Do not hold the key in a variable. If you must, use set +x before the command, or use cat with a heredoc (though the variable is still in memory).
Resolution
Bounce Team: The issue was resolved in PR#98.
-
L-07 Low Potential Private Key Exposure Via Shell Memory Best Practices Resolved
Description
The architectural decision to handle the private key as a shell variable (
$private_key) before writing it to a file negates the primary security benefit of file-based secret handling.By accepting the key as a function argument (local
private_key=$4), the secret is loaded into the shell process's memory. If the script crashes and generates a core dump, or if the environment is exported for debugging, the key is exposed in plaintext.Depending on how the script invokes other commands, there is a risk that the variable could be implicitly passed to child processes or visible in the
/proc/{pid}/environfile of the running shell (though local mitigates this, it does not eliminate the memory footprint).The secret exists in the application layer for the entire duration of the variable's lifecycle, rather than only existing on disk with restricted permissions.
Recommendation
- Refactor the upstream logic (e.g., the Python script) to write the secret directly to a secure file.
- The shell script should only ever handle the file path string, never the secret content itself.
Resolution
Bounce Team: The issue was resolved in PR#106.
-
L-08 Low Insecure File Creation Window (Race Condition) Unexpected Behavior Resolved
Description
There is a Time-of-Check to Time-of-Use (TOCTOU) style race condition between the creation of the temporary file and the application of secure permissions.
The Gap:
mktempcreates the file using the current system umask (often 0022, making files world-readable by default). The script then runs chmod 600 in a subsequent command.Between the execution of
mktempandchmod, there is a non-zero time window where the file exists on the filesystem with loose permissions.A malicious actor or compromised process monitoring
/tmp(using inotify or a busy-loop) could open a file handle to read the content the moment the file is created, effectively bypassing the subsequentchmod.Recommendation
- Ensure atomic secure creation.
Option 1 (Umask): Set a strict umask immediately before creation:
old_umask=\$(umask) umask 077 temp_key_file=\$(mktemp) umask \$old_umaskOption 2: As recommended in earlier issue, let Python handle this using
os.openwith specific mode flags (0o600) to ensure the file is never created with insecure permissions.Resolution
Bounce Team: The issue was resolved in PR#99.
-
L-09 Low "Fail Open" On DNS Failure Error Resolved
Description
The code explicitly allows URLs to pass validation if DNS resolution fails. This negates the security control.
except socket.gaierror: return True, None # <--- VULNERABILITYAn attacker can provide an internal hostname that your specific validation environment can't resolve (or they can make their DNS server time out intentionally).
The check returns
True. Later, the actual HTTP client (which might have different timeout settings or cached DNS entries) successfully resolves the internal address and performs the SSRF attack.Recommendation
Security controls must Fail Closed. Instead of return
True.return False, "DNS resolution failed;cannot verify safety."Resolution
Bounce Team: The issue was resolved in PR#100.
-
L-10 Low Potential DNS Rebinding Vulnerability (TOCTOU) Best Practices Acknowledged
Description
This implementation suffers from a classic Time-of-Check to Time-of-Use (TOCTOU) vulnerability.
You are validating the DNS record at a specific point in time, but you are not enforcing that the subsequent HTTP request uses that exact same IP address.
Time of check: If the code calls
socket.gethostbyname(hostname). The attacker's DNS server returns a safe IP (e.g., 8.8.8.8) with a TTL (Time To Live) of 0 seconds. The validation passes.Gap: The code returns True. The application logic proceeds to make the actual RPC call (using requests, aiohttp, or
web3.py).Time of use: Time of Use: Because the TTL was 0, the HTTP client triggers a new DNS resolution. The attacker's DNS server detects the second query and now returns a private IP (e.g., 127.0.0.1 or 169.254.169.254).
As a result, the HTTP client connects to the internal resource, bypassing your security check entirely.
Recommendation
- Do not rely on the hostname for the actual connection.
- Resolve the IP address.
- Validate the IP address.
- Force the connection to the IP: Construct the URL using the validated IP address (e.g.,
https://1.2.3.4/rpc) and manually set the Host header to the original hostname so the server accepts the request (SNI/Virtual Host compatibility).
Resolution
Bounce Team: Acknowledged.
-
L-11 Low Thread-Unsafe Global State Mutation Best Practices Resolved
Description
The function modifies
socket.setdefaulttimeout(), which is a process-wide global setting in Python.original_timeout = socket.getdefaulttimeout() try: socket.setdefaulttimeout(5) # <--- Global lock on all new sockets finally: socket.setdefaulttimeout(original_timeout)In a multi-threaded application (common in frameworks handling RPCs, database connections, or API endpoints):
Interference: If Thread A is running this validation, it sets the global timeout to 5 seconds.
Collateral Damage: If Thread B starts a database connection, a Redis operation, or a long-polling request at that exact moment, it inherits the 5-second timeout. This can cause random
TimeoutErrorexceptions in completely unrelated parts of your application that expect different timeout behaviors (or no timeout at all).Race Conditions: If two threads run this validation function simultaneously, they will fight over the
original_timeoutvalue, potentially permanently leaving the application in a state with a 5-second global timeout.Recommendation
Never change global defaults for local operations. Use
socket.create_connection(which accepts a timeout argument) or generic DNS libraries that allow context-specific timeouts.Resolution
Bounce Team: The issue was resolved in PR#101.
-
L-12 Low IPv6 Validation Bypass Logical Error Resolved
Description
The code relies exclusively on
socket.gethostbyname(), which is a legacy function that typically resolves only IPv4 addresses (A records).The Mechanism:
- Modern operating systems and networking libraries (like requests or
urllib3) prefer IPv6 over IPv4
("Happy Eyeballs" algorithm).
- If an attacker configures a domain dualstack.evil.com with:
A Record (IPv4): 1.1.1.1 (Public/Safe)AAAA Record (IPv6): ::1 (Localhost/Unsafe)The Bypass:
- The validation calls gethostbyname, sees 1.1.1.1, and returns Valid.
- The HTTP client performs the request, prefers the AAAA record, and connects to ::1.
- The request hits your local internal network.
Recommendation
Use
socket.getaddrinfoinstead ofgethostbyname. You must iterate through every returned IP address (bothAF_INETfor IPv4 andAF_INET6for IPv6) and ensure all of them are non-private. If even one resolution points to a private network, the URL must be rejected.Resolution
Bounce Team: The issue was resolved in PR#101.
- Modern operating systems and networking libraries (like requests or
-
L-13 Low Flawed Deduplication Logic In Telegram Alerting Logical Error Resolved
Description
The
_is_duplicate_alertmethod determines uniqueness based solely on metadata (process_name,instance_name,logger_name, and contexts). It explicitly ignores the message (body) of the alert.The service will silently drop critical alerts. If a specific logger emits a generic error (e.g., "Connection Start"), followed immediately by a critical error (e.g., "Critical Data Corruption"), the second error will be discarded as a duplicate because the metadata context is identical.
Recommendation
Include a hash of the message body in the deduplication comparison.
Resolution
Bounce Team: The issue was resolved in PR#102.
-
I-01 Informational Unnecessary Gas Spent For Executions Gas Optimization Resolved
Description
When executing pending redemptions, the service now verifies if at least one user redemption in the list will be executable. Although the transaction will succeed, it will waste unnecessary gas for the non-executable users when looping through all user list.
Recommendation
Consider filtering the redemptions that will be executed, using the simulation inside
can_execute_redemptions.Resolution
Bounce Team: The issue was resolved in PR#104.
-
I-02 Informational Unsupported Leveraged Tokens Informational Acknowledged
Description
The
PerpAssetConfignow verifies if:1 - position to open above a % of the Hyperliquid max perp leverage (currently 90% )
2 - token leverage above max TVL leverage (6.3x)
However, the factory contract does not enforce any of this. If an unsupported token is deployed (i.e. SOL10L), users can mint LTs but the automation scripts will never support it.
Keep in mind that tokens like SOL and HYPE have a perps max leverage of 10x, so they are right at the limit.
Recommendation
Consider either documenting these validations in the
Factorycontract or enforcing it via config params during_validateLeverage.Resolution
Bounce Team: Acknowledged.
-
I-03 Informational Deployment Script Private Key Handling Best Practices Resolved
Description
Deployment script loads the private key via raw
PRIVATE_KEYenvironment variable. Raw private keys are easily leaked through shell history, process lists, CI logs, or accidentally committed.envfiles.Recommendation
Switch to Foundry’s encrypted keystore more info:
https://getfoundry.sh/guides/best-practices/key-management/#using-a-keystore
Resolution
Bounce Team: The issue was resolved in PR#151.
-
I-04 Informational Insecure Private Key Handling Best Practices Resolved
Description
The script stores the generated private key in shell variables, writes it to .env, and passes it as a command-line argument. This leaks the key via ps aux, shell history, logs, and version control.
Recommendation
Never let the private key touch the shell. Generate and upload it directly from Python (or use Foundry/cast keystore), return only the address, and store just the address/reference in .env and YAML files.
Resolution
Bounce Team: The issue was resolved in PR#106.
-
I-05 Informational Gaming The Order Of The Redemption Queue Logical Error Acknowledged
Description
executeRedemptions()processespendingRedemptionsarray sequentially. In_removeCredit(), swap-and-pop removal allows attackers to front-run executions:by timing a new redemption as the last element, popping shifts them to an earlier position (e.g., front), enabling order manipulation.
Recommendation
Replace with order-preserving queue handling/Execute the redemptions ordered by the
pendingRedemptionTimestampset during prepareResolution
Bounce Team: Acknowledged.
-
I-06 Informational Potential Info Disclosure Through Telegram API Best Practices Resolved
Description
The service blindly forwards error logs to Telegram without inspecting the content for sensitive information.
Developers often log exceptions that include full context. For example, a database connection error might include the connection string (with password), or an API error might dump the request headers (with Bearer tokens).
An attacker who gains access to the Telegram channel (or the Telegram servers' history) can view these secrets.
Recommendation
Implement a "Redactor" or "Sanitizer" utility that regex-matches and masks patterns like
API_KEY,Bearer,password=, and private key formats before the message is formatted.Resolution
Bounce Team: The issue was resolved in PR#105.
-
I-07 Informational Missing API Rate Limiting In Telegram Alerting Best Practices Resolved
Description
The service does not implement client-side rate limiting. Telegram enforces strict limits (~30 msgs/sec globally, ~1 msg/sec per chat).
During an error burst (e.g., database downtime), the service will flood the API, receive
429 Too ManyRequests errors, and likely throw exceptions in the handler. This can trigger an infinite feedback loop where the Alerting Service logs errors about failing to send errors.Recommendation
Implement a "Leaky Bucket" rate limiter or a "Circuit Breaker" to pause notifications when limits are reached.
Resolution
Bounce Team: The issue was resolved in PR#103.
No findings match.
More from Bounce Tech
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.