Date of engagement: 13th May 2025 - 26th May 2025
-
Timur GüvenkayaPrincipal advisor
Engagement Dashboard
Audit results
Guvenkaya conducted a security assessment of the Cleopetra Solana Trading Bot from 13th May 2025 to 26th May 2025. During this engagement, a total of 12 findings were reported. 6 of the findings were medium and the remaining were either low or informational severity. All major issues are either fixed or acknowledged by the Cleopetra team.
Engagement record
- Client
- Cleopetra
- System
- Cleopetra Solana trading bot
- Review period
- 13th May 2025 - 26th May 2025
- Reviewed baseline
f22cf033a335d2717245fbc8ba7cd327a0215eb2- Paths in scope
- 10
- Principal advisor
- Timur Güvenkaya
Timeline
- Review start13th May 2025
- Draft report26th May 2025
- Final report21st June 2025
Findings by severity
Finding status at final report
Remediation complexity
Impact
Likelihood
Advisory and Client
The advisory
Guvenkaya specializes in security for digital assets and financial infrastructure, from smart contracts and protocols to applications, infrastructure, custody and cryptography.
Our team, including the sitting Heads of Security at Gauntlet and Agora, brings experience from Kraken, OpenZeppelin, Binance, ZKsync, Nillion, Invicti and EY. Across the team: 200+ security reviews, $30B+ in volume secured, 13 published cryptography research papers, and credentials including OSCP, OSWE and CISM.
- Every reviewer has a public bio and is named before you sign. We match expertise to your system, with a principal leading scoping and checking findings.
The client
Cleopetra is a seamless platform for liquidity provisioning on Solana DEXes, directly from Telegram. Enter a token, pick an LP strategy of your choice and create the position - the agent finds optimal pools and auto-rebalance positions when needed to maximise fees and reduce impermanent loss.
Scope and Review Boundaries
Review depths
- Deep
- Line-by-line review with explicit attack hypotheses, adversarial reasoning about state transitions, and reproduction attempts for each hypothesis that survived reading.
- Supporting
- The code was read for its effect on a reviewed flow. Behaviour outside that effect was not assessed.
- Targeted
- Entry points were exercised or triaged against the flow's security properties rather than read in full.
This is the code we read, and how closely. 10 paths are in scope. Each path links to the file at the reviewed commit.
| Path | Review depth | Scope note |
|---|---|---|
src/ | Deep | Bot |
src/ | Deep | Cron |
src/ | Deep | DB |
src/ | Deep | Helpers |
src/ | Deep | Config |
src/ | Deep | Constants |
src/ | Deep | Index |
src/ | Deep | Providers |
src/ | Deep | Types |
src/ | Deep | Utils |
Out of scope
The audit will include reviewing the code for security vulnerabilities. The audit does not include a review of the tests, deployment setup, architecture, and dependencies.
Assessment Framework
Guvenkaya rates findings by likelihood and impact so remediation can be prioritized by technical risk, business context, and the decision the engagement is meant to support.
Rating chain applied to every finding
- 01 Likelihood Likely, possible or rare
- 02 Impact Severe, moderate or negligible
- 03 Severity Derived from the matrix below
- 04 Remediation complexity Simple, moderate or difficult
- 05 Status Not fixed, fixed, acknowledged or scheduled
Impact
| Impact | What it means here |
|---|---|
| Severe | Exploitation could result in critical loss or compromise, such as full system control, substantial financial loss, or severe reputational damage. |
| Moderate | Exploitation may lead to limited data loss, partial compromise, moderate financial impact, or noticeable degradation of services. |
| Negligible | Exploitation has minimal impact, such as minor data exposure without significant consequences or slight inconvenience without substantial disruption. |
Likelihood
| Likelihood | What it means here |
|---|---|
| Likely | The vulnerability is easily discoverable and not overly complex to exploit. |
| Possible | The vulnerability presents some challenges either in discovery or in the complexity of the attack. |
| Rare | The vulnerability is either very difficult to discover or complex to exploit, or both. |
This matrix provides a nuanced view, taking into account both the ease of discovering a vulnerability and the complexity involved in exploiting it.
| Attack Complexity \ Discovery Ease | Obvious | Concealed | Hidden |
|---|---|---|---|
| Complex | Possible | Rare | Rare |
| Moderate | Likely | Possible | Rare |
| Straightforward | Likely | Possible | Possible |
Severity
| Severity | What it means here |
|---|---|
| Critical | Assigned to vulnerabilities with severe impact and a likely likelihood of exploitation. |
| High | For vulnerabilities with either severe impact but only a possible likelihood, or moderate impact with a likely likelihood. |
| Medium | Used for vulnerabilities with severe impact but a rare likelihood, moderate impact with a possible likelihood, or negligible impact with a likely likelihood. |
| Low | For vulnerabilities with moderate impact and rare likelihood, or negligible impact with a possible likelihood. |
| Informational | The lowest severity rating, typically for vulnerabilities with negligible impact and a rare likelihood of exploitation. |
| Likelihood \ Impact | Severe | Moderate | Negligible |
|---|---|---|---|
| Likely | Critical | High | Medium |
| Possible | High | Medium | Low |
| Rare | Medium | Low | Informational |
Remediation complexity
This measures how difficult it is to remediate a finding once it has been identified.
| Remediation complexity | What it means here |
|---|---|
| Simple | Patches or fixes are readily available and easily implemented. |
| Moderate | Requires some time and resources to remediate, but well within the capabilities of most organizations. |
| Difficult | Remediation requires significant resources, specialized skills, or substantial changes to systems or architecture. |
Finding status
This tracks the client's response to each finding and whether remediation has been completed, accepted, or scheduled.
| Status | What it means here |
|---|---|
| Not Fixed | Indicates that the vulnerability has been identified but no remedial action has been taken yet. This status is crucial for newly discovered vulnerabilities or those awaiting prioritization. |
| Fixed | This status is applied when the vulnerability has been successfully remediated. It implies that appropriate measures, such as patching, configuration changes, or architectural modifications, have been implemented to resolve the issue. |
| Acknowledged | This status is used for vulnerabilities that have been recognized, but for various reasons (such as risk acceptance, cost, or other business decisions), have not been fixed. It indicates that the risk posed by the vulnerability is known and has been consciously accepted. |
| Scheduled | This status indicates that the vulnerability has been acknowledged and a plan is in place to fix it in the future. It signifies that while remediation hasn't yet occurred, the issue has been prioritized and is part of the planned development roadmap. |
Findings Summary
Findings register
Detailed Findings
GUV-1: Incorrect Use of Async in Reward Distribution
Description
The distributeRewardsJob uses Map.forEach(async () => ) to group referrers and send notifications. But forEach doesn’t wait for async code to finish. This means important steps like building the reward batches and notifying users might still be running while the function moves on to sending transactions and updating state.
This can cause multiple issues, such as:
- Some users might not get the rewards they earned.
- Shared data might be changed at the same time from different places, leading to bugs or incorrect results.
- The link between who referred whom and what rewards they got might break.
- Rewards could be lost, database records might be wrong, and users could lose trust in the system.
Proof of concept: GUV-1 PoC
Recommendation
Replace all map.forEach(async ... usage for asynchronous workflows with sequential for...of loops or bulk Promise.all() approach. Carefully construct batches first, then proceed to transactional stages.
Remediation
The Cleopetra team has fixed this issue in this PR: #93
GUV-2: Possible Loss of Funds Through Invalid State Update
Description
Reward distribution job attempt to sign and send multiple transactions in a loop, pushing each signature to an array. However, there is no granular verification or handling for failed transactions per user/referrer. If sendAndConfirmTransaction fails for a batch or individual reward payout, downstream logic still marks rewards as processed and notifies users, even if the transaction failed on-chain.
Proof of concept: GUV-2 PoC
Recommendation
Track transaction success/failure and only mark as processed those fees which actually receive confirmed on-chain rewards and for failed transactions, implement retry logic or mark for audit/reprocessing.
Remediation
The Cleopetra team has fixed this issue in this PR: #93
GUV-3: Insecure Private Key Export Flow
Description
The exportPrivateKey command allows users to export their (Privy-based) private keys directly through a Telegram message with:
await ctx.reply('✅ Successfully exported private key - '${privateKey}' Import private key into wallets like Phantom (tap to copy) and delete this message once you are done.', { parse_mode: "Markdown" }); Exported keys are sent as plaintext in a chat message, putting them at risk from Telegram/Telegram client interception and device compromise. The export implementation in /src/helpers/privy.ts is invoked directly on request, bypassing secure delivery or split-secret protections.
Recommendation
it is imperative to implement a mandatory secondary authentication system PIN OR a user-defined passphrase before allowing any sensitive operations such as private key export.
During the initial wallet setup, require users to create a secure PIN OR passphrase and enforce a multi-step confirmation flow:
- Prompt users to re-enter their PIN/passphrase before proceeding.
- Display a strong security warning with a deliberate confirmation step (e.g., type “CONFIRM”).
The Cleopetra team has fixed this issue in this PR: #99
GUV-4: No Encryption-at-rest for Sensitive User Data
Description
The PostgreSQL schema for users stores privyWalletAddress,privyWalletId, and possibly private key export activity (didExportPvtKey) in plaintext (see /src/db/schema.ts). There is no evidence of encryption-at-rest, field-level encryption for sensitive columns, or defense-in-depth for backend storage. While wallet private keys are not directly stored, IDs are. If the database is compromised (insider threat, credential leak, SQLi), attackers may tamper with the wallet ids and drain user funds.
Recommendation
- Encrypt sensitive user columns at rest with application-level encryption (encryption/decryption key separate from DB, e.g. KMS-backed).
- Ensure database is encrypted on-disk using PostgreSQL's built-in encryption or cloud-provided at-rest encryption.
- Only expose Privy wallet IDs for short-lived server processes; avoid long-term storage unless required.
Remediation
The Cleopetra Team has scheduled the fix.
GUV-5: Database Tampering Risk
Description
The current implementation of the bot stores both Privy wallet IDs and addresses in the database. This creates a security risk where an attacker who gains database access could modify wallet addresses while keeping the original wallet IDs. This would allow them to execute transactions that are signed by legitimate wallets but paid from attacker-controlled addresses for example when user calls closePosition function that would affect all position management operations including closing positions and can exploited by anyone with database write access.
Recommendation
- Encrypt sensitive user columns at rest with application-level encryption (encryption/decryption key separate from DB, e.g. KMS-backed).
- Ensure database is encrypted on-disk using PostgreSQL's built-in encryption or cloud-provided at-rest encryption.
- Only expose Privy wallet IDs for short-lived server processes; avoid long-term storage unless required.
CREATE OR REPLACE FUNCTION prevent_privy_wallet_id_update()RETURNS TRIGGER AS $$BEGIN IF NEW.privy_wallet_id IS DISTINCT FROM OLD.privy_wallet_id THEN RAISE EXCEPTION 'privy_wallet_id is immutable and cannot be changed'; END IF; RETURN NEW;END;$$ LANGUAGE plpgsql; Remediation
The Cleopetra Team has fixed the by adding a trigger which restricts update for privy_wallet_id column and restrict regular db users to alter/drop that function.
GUV-6: Potential Private Key Leakage in Error Logging
Description
The private key export functionality contains a security issue in the exportPrivateKey function where sensitive information about private key operations is might be logged in error messages and this concerning because the error message from the underlying operation could potentially contain sensitive information about the private key export process
console.log( 'an error occurred while exporting pvt key for ${walletId} wallet - ${e.message}' ); throw err; Recommendation
Remove or sanitize the error logging to avoid exposing sensitive information in private key export functionality OR use a dedicated error logging service that can automatically sanitize sensitive information.
Remediation
The Cleopetra team has fixed this issue in this PR: #93
GUV-7: Incorrect Units for changing priority fees
Description
The changePriorityFees accepts a SOL amount typed by the user, parses it with parseFloat, stores the raw value directly in users.computeUnitsPrice, and later re-uses it as lamports/µ-lamports inside transaction-building logic:
const priorityFees = parseFloat(ctx.message.text);await db.update(usersTable).set({ computeUnitsPrice: priorityFees }) The value is then consumed as:
microLamports: cuPrice * 0.3, // expects µ-lamports per CUlamports: cuPrice * 0.7 // Jito tip transfer (expects lamports) since cuPrice is supposed to represent micro-lamports / CU (i.e. 1e-6 SOL units). The same value is blindly multiplied by 0.7 and used as a direct lamport transfer to the Jito tip account. For the default MEDIUM_PRIORITY the bot tips 0.07 SOL per transaction (100 000 lamports × 0.7), more than 700× the intended 0.1 lamport/CU.
Recommendation
Store computeUnitsPrice exclusively in µ-lamports (integer). Convert SOL → µ-lamports when accepting human input: Math.floor(inputSol * LAMPORTS_PER_SOL / 1_000_000).
Remediation
The Cleopetra team has fixed this issue in this PR: #93
GUV-8: Potential Race Condition in Mass Operations
Description
When performing mass-operations ("close all positions", "claim all fees"), the bot executes actions in parallel (Promise.all) on a mutable ctx.session.positions array. If array/context is modified mid-operation (by another event, user spam, concurrent callback, or user race), there could be:
- Operations on non-existent/unstable state (double close, duplicate claim, non-existent index).
- State desync leading to incorrect user feedback, failed tx, or even race-triggered wallet logic bugs.
Example - Close All Positions (portfolio:close_all:yes callback):
const promises = ctx.session.positions.map((position, i) => { return new Promise<void>(async (resolve, _) => { // ... position closing logic ... });});await Promise.all(promises); There's no locking mechanism to prevent concurrent mass operations the array could be modified by other operations while these mass operations are running
Recommendation
create a lock mechanism to prevent concurrent mass operations, below is example fix for Close All Positions it's recommended to do the same for all Mass Operations like:
- Claim All Fees
- Mass DM
Remediation
The Cleopetra Team has acknowledged the issue.
GUV-9: Insufficient Onchain Transaction Confirmation
Description
Transactions are sent and confirmation is checked (confirmTransaction, parseTxn) for "confirmed" status, but in Solana, "confirmed" is not durable finality ("finalized" is safest). The bot proceeds to update database state (positions, fees, etc.) and notifies users immediately on "confirmed", which may be reverted or dropped in rare Solana edge cases (such as forks or cluster issues). There is no check for "finalized" status, and database updates are not re-synced if transactions revert later.
const metadata = await connection.getParsedTransaction(sig, { commitment: "confirmed", maxSupportedTransactionVersion: 0,}); Recommendation
Use "finalized" commitment instead of "confirmed".
Remediation
The Cleopetra Team has acknowledged the issue.
GUV-10: Preventing Possible Insecure Direct Object Reference
Description
While the current implementation in our Telegram bot doesn't present an immediate IDOR vulnerability (as parameters are controlled through Telegram's interface), it's a security best practice to implement defense in depth by enforcing strict ownership verification for all database operations. This approach ensures that even if the application's interface changes or new access points are added in the future, the underlying data access layer maintains proper security controls.
Examples from the current implementation missing explicit ownership verification:
const positionInfo = await db.query.positionsTable.findFirst({ where: eq(positionsTable.address, positionAddress) }); await db .update(positionsTable) .set({ rebalancingEnabled: !positionInfo.rebalancingEnabled }) .where(eq(positionsTable.address, ctx.session.positionAddress)); Recommendation
This how the recommended implementation would look like
const positionInfo = await db.query.positionsTable.findFirst({ where: and( eq(positionsTable.address, positionAddress), eq(positionsTable.owner, ctx.session.wallet) )}); Remediation
The Cleopetra team has fixed this issue in this PR: #93
GUV-11: Lack Of Health/Execution Logging and Error Recovery
Description
In the cronjobs Logging is limited to console.log statements, with some errors only sent as Discord messages. There is no structured health monitoring, persistent error reporting/retries, or success/failure audit logs outside of ephemeral console/Discord. Issues may therefore go undetected or unresolved:
- Missed/failed cron runs.
- Partial batch failures.
- Recurring errors causing fund or data integrity issues
Recommendation
- Integrate
DataDogor Sentry for comprehensive monitoring using the typescript SDK. - Add structured logging with consistent format including timestamps.
- Implement retry mechanisms with exponential backoff Use existing
retryFuncWrapper
Remediation
The Cleopetra team has fixed this issue in this PR: #93
GUV-12: Inadequate Confirmation for Token Recipient and Amount
Description
Before final transmission of funds (SOL or SPL tokens) to the user-provided vault address: The bot does not explicitly show a "confirm transfer" summary of critical details (recipient, token/mint, amount/decimal) once more before sending. Users may make mistakes due to typos, malware-autofill, or clipboard hijacking, leading to fund loss.
Recommendation
- Insert an explicit human-confirmation step after collecting all transfer details (recipient, token, amount), before executing the blockchain transaction. Require the user to click a "confirm" button, displaying full destination address and parsed amount.
- Provide a last-moment "cancel" option in the confirmation step.
Remediation
The Cleopetra team has fixed this issue in this PR: #96
The web version and the PDF (20 pages) are generated from the same source. The report as first delivered is kept in the public reports archive.
