Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · February 2026

Protocol Review

for Degen Safe

Degensafe engaged Guardian to review the security of their Degensafe. From the 8th of December 2025 to the 11th of December 2025, a team of 2 auditors reviewed the source code in scope.

Published
Review window
December 8 to 11, 2025
Language
Rust
Chains
Solana
Sector
Token launches, Staking
  • 5 Critical
  • 2 High
  • 4 Medium
  • 9 Low
  • 7 Informational

24 resolved · 3 partially resolved

Scope

Overview

Degensafe engaged Guardian to review the security of their Degensafe. From the 8th of December 2025 to the 11th of December 2025, a team of 2 auditors reviewed the source code in scope.

Findings 27

  1. C-01 Critical Anyone Can Takeover Pool And Corrupt State Logical Error Resolved
    Location
    staking.rs: 10

    Description

    The create_pool instruction uses init_if_needed for the pool account, which allows the instruction to succeed even when the pool already exists. While init_if_needed prevents re-allocation of an existing account, the function unconditionally overwrites all pool state:

    pool.owner = maybe_owner.unwrap_or(ctx.accounts.admin.key());
    pool.token_mint = ctx.accounts.token_mint.key();
    pool.reward_mint = ctx.accounts.reward_mint.key();
    pool.reward_percentage = reward_percentage;
    pool.total_staked = 0;
    pool.bump = ctx.bumps.pool;
    pool.reward_vault = ctx.accounts.reward_vault.key();
    pool.is_active = true;
    

    There is no check verifying that the pool is uninitialized or that the caller is the existing owner. Any user can call create_pool on an existing pool and:

    • Seize ownership to set themselves as pool.owner
    • Reset total_staked to 0 -> causes underflow panics on subsequent withdrawals when

    pool.total_staked.checked_sub(amount) is called

    • Change reward_mint and reward_vault -> redirect rewards or cause withdrawal failures
    • Modify reward_percentage -> manipulate reward calculations

    Recommendation

    Replace init_if_needed with init to ensure the instruction fails if the pool already exists.

    Resolution

    Degensafe Team: Resolved.

  2. C-02 Critical Attacker Can Drain Arbitrary User Stake Logical Error Resolved
    Location
    staking.rs: 245

    Description

    The withdraw_stake instruction only checks that user_stake.amount >= amount and lacks any further validation on the user_stake account. An attacker can exploit this by passing any victim's UserStake account while providing their own token accounts as the withdrawal destination.

    Ultimately, an arbitrary attacker can steal other user's tokens/rewards to their own accounts, and drain arbitrary pools.

    Recommendation

    Add constraints that user_stake.owner == user.key() and user_stake.pool == pool.key().

    Resolution

    Degensafe Team: Resolved.

  3. C-03 Critical Vault PDA Collision Enables Cross-Pool Theft Logical Error Resolved
    Location
    staking.rs: 335

    Description

    The reward_vault PDA is derived only from the reward mint, not the pool. When multiple pools use the same reward token, they all share a single vault, but only the first pool to create it becomes the token account authority.

    Consider the following scenario:

    • Admin A creates Pool A with token_mint = SOL and reward_mint = USDC
    • The reward_vault PDA is created with Pool A as authority
    • Admin B creates Pool B with token_mint = ETH and reward_mint = USDC
    • The vault already exists; init_if_needed skips initialization
    • Pool A remains the authority
    • Admin B deposits 100,000 USDC rewards into the shared vault
    • Admin A calls withdraw_reward for 100,000 USDC
    • The program signs with Pool A's seeds, which matches the vault authority
    • Admin A receives all of Pool B's rewards

    Ultimately Pool B's users stake tokens expecting rewards, but the vault has been drained by an unrelated pool owner.

    Recommendation

    Include the pool address in the reward vault PDA seeds to ensure each pool has its own isolated vault

    Resolution

    Degensafe Team: Resolved.

  4. C-04 Critical Vault Authority Takeover Logical Error Resolved
    Location
    lc_deposit.rs: 14

    Description

    The initialize instruction uses init_if_needed for the vault_state account without verifying whether the vault has already been initialized. While init_if_needed prevents re-allocation of an existing account, it does not prevent initialize from being called again and overwriting fields on an existing account.

    Any user can call initialize on an existing vault and: 1. Seize ownership -> overwrite authority with their own pubkey 2. Set wallet_account to themselves -> drain all tokens to themselves through withdraw 3. Corrupt accounting -> reset balance to 0

    Once an attacker becomes the authority, they gain full control over set_withdrawal_account and withdraw, allowing them to drain all deposited funds.

    Recommendation

    Replace vault_state's init_if_needed with init to ensure the instruction fails if the vault already exists or validate against an is_initialized boolean which is set to true on the first initialize call.

    Resolution

    Degensafe Team: Resolved.

  5. C-05 Critical Authority Hijacking In SOL Deposit Logical Error Resolved
    Location
    sol_deposit.rs: 11

    Description

    The initialize function uses init_if_needed to create the vault state account, but unconditionally overwrites all state fields including the authority on every call.

    Since the vault state is a PDA derived from a static seed (b"vault_state"), the account will already exist after the first legitimate initialization.

    However, because init_if_needed allows the instruction to succeed even when the account exists, any attacker can call initialize() to overwrite the authority field with their own public key.

    This grants the attacker full administrative control over the vault, allowing them to set the withdrawal wallet and drain all deposited funds.

    Recommendation

    Replace init_if_needed with init to ensure the instruction can only succeed once.

    Resolution

    Degensafe Team: Resolved.

  6. H-01 High Varied Reward And Deposit Token Leads To Drain Logical Error Resolved
    Location
    staking.rs: 606

    Description

    The reward calculation treats staked token base units as equivalent to reward token base units, with no price or value conversion. This fundamental flaw causes mispayments when the reward token has a different value than the staking token.

    The formula simply computes: reward = staked_amount * percentage * time / year This assumes 1 base unit staked = 1 base unit rewarded, ignoring that tokens have entirely different valuations.

    Underpayment Scenario:

    • Staking token: WBTC (8 decimals, ~$100,000 per coin)
    • Reward token: USDC (6 decimals, $1 per coin)
    • Pool configured with 10% APY
    • User stakes 1 WBTC = 100,000,000 base units ($100,000 value)
    • After 1 year, reward calculation: 100,000,000 * 10 / 100 = 10,000,000 base units
    • Result: User receives 10,000,000 USDC base units = 10 USDC

    Expected: $10,000 in rewards (10% of $100,000) Actual: $10 in rewards

    Overpayment Scenario:

    • Staking token: USDC (6 decimals)
    • Reward token: WBTC (8 decimals)
    • User stakes 1,000 USDC = 1,000,000,000 base units ($1,000 value)
    • After 1 year: 1,000,000,000 * 10 / 100 = 100,000,000 base units
    • Result: User receives 100,000,000 WBTC base units = 1 WBTC = $100,000

    Expected: $100 in rewards (10% of $1,000) Actual: $100,000 in rewards — a 1000x overpayment

    A single user staking $1,000 USDC for one year drains $100,000 from the reward vault.

    Recommendation

    Implement a price oracle or admin-configured exchange rate to convert between staking token value and reward token value. Alternatively, restrict the protocol to same-token staking and rewards

    Resolution

    Degensafe Team: Unresolved.

  7. H-02 High Deposit Record Grief Through Order ID Creation Griefing Resolved
    Location
    Global

    Description

    The DepositRecord PDA seeds in both lc_deposit and sol_deposit do not include the depositor's address, allowing any user to claim arbitrary order_id values and block legitimate deposits.

    In lc_deposit, the seeds are: seeds = [b"deposit_record", vault_state.token_mint.as_ref(), order_id.as_bytes()].

    In sol_deposit, the seeds are: seeds = [b"deposit_record", order_id.as_bytes()].

    Since user is not part of the PDA derivation, an attacker can front-run or preemptively create deposit records for any order_id with minimal cost. The init constraint causes the victim's transaction to fail when the PDA already exists, blocking specific users from depositing.

    This is especially impactful since token creation fees must be paid in both sol_deposit and lc_deposit each time, and the order_id’s must match between the two programs.

    Therefore, after a user deposits in one vault, an attacker can take the order_id in the other vault for minimal cost — preventing the token creation process from fully going through and locking user funds.

    Recommendation

    Consider including the user's pubkey in the PDA seeds.

    Resolution

    Degensafe Team: Resolved.

  8. M-01 Medium Reward Rate Changes Apply Retroactively Logical Error Resolved
    Location
    staking.rs: 88

    Description

    The reward calculation uses the current reward_percentage multiplied by the entire elapsed time since last_staked_time. When an admin updates the reward rate, it retroactively applies to all previously accrued but unclaimed rewards.

    Consider the following scenario:

    • User stakes 10,000 tokens at 20% APY
    • User waits 364 days, expecting ~2,000 tokens in rewards
    • Admin changes rate to 1% APY on day 364
    • User withdraws on day 365
    • Calculation uses 1% for the entire 365 days
    • User receives ~100 tokens instead of ~2,000

    Recommendation

    Snapshot pending rewards before applying rate changes.

    Resolution

    Degensafe Team: Resolved.

  9. M-02 Medium Withdrawal Destination Not Validated Logical Error Resolved
    Location
    sol_deposit.rs: 61

    Description

    The withdraw function checks that a withdrawal wallet has been configured in the vault state, but the wallet_account passed into the instruction is never validated against this stored value. The account is marked with /// CHECK and only constrained to be mutable.

    The authority can pass any arbitrary address as wallet_account, and the funds will be transferred to that address regardless of what was configured with set_withdrawal_account, rendering the withdrawal wallet configuration meaningless.

    Recommendation

    Add has_one = wallet_account to the vault_state account constraints in the Withdraw struct.

    Resolution

    Degensafe Team: Resolved.

  10. M-03 Medium Initialize Frontrunning Risk Frontrunning Resolved
    Location
    Global

    Description

    Deployment of the program and the call to initialize on Solana are not atomic -- meaning the program is first deployed, and initialize must be called in a separate transaction.

    Since initialize lacks any access control, anyone can call it immediately after deployment. A malicious actor could front-run the intended initializer, set the authority to their own address, and take full control of the protocol’s configuration and operations.

    Recommendation

    Deploy and call initialize in the same transaction using a deployment script.

    Resolution

    Degensafe Team: Unresolved.

  11. M-04 Medium Insufficient Rewards Block Principal Withdrawal DoS Resolved
    Location
    staking.rs: 262

    Description

    In withdraw_stake, the function checks if the reward vault has sufficient balance to pay rewards before allowing any withdrawal:

    require!(
    ctx.accounts.reward_vault.amount >= reward_to_send,
    CustomError::InsufficientRewardVault
    );
    

    If the reward vault is underfunded, users cannot withdraw their staked principal even though the principal tokens exist in a separate pool_vault. This creates a situation where user funds can be locked if the admin fails to maintain adequate reward token reserves or drains the reward vault.

    Recommendation

    Consider allowing principal withdrawal even when rewards cannot be paid. Otherwise, clearly document this risk.

    Resolution

    Degensafe Team: Resolved.

  12. L-01 Low Fractional Reward Rates Not Supported Logical Error Resolved
    Location
    staking.rs: 630

    Description

    The reward_percentage field is stored as a u64 and interpreted as a whole number percentage, dividing by 100 in the reward calculation. This prevents configuring fractional APY rates (e.g. 5.5%).

    Recommendation

    Consider using basis points instead of percentages directly.

    Resolution

    Degensafe Team: Resolved.

  13. L-02 Low Withdrawal Wallet Argument And Account Mismatch Validation Resolved
    Location
    lc_deposit.rs: 63

    Description

    The set_withdrawal_account function accepts both a new_wallet: Pubkey argument and a ctx.accounts.new_wallet account. The vault state stores the argument value, but the ATA creation uses the account as the authority.

    If the caller passes mismatched values:

    • new_wallet arg = address A
    • ctx.accounts.new_wallet account = address B

    The vault state will point to A, but the ATA is created for B. When withdraw is called, it expects an ATA with authority matching vault_state.wallet_account (A), which doesn't exist. Withdrawals fail until the admin calls set_withdrawal_account again with matching values.

    Recommendation

    Enforce the account matches passed new_wallet argument.

    Resolution

    Degensafe Team: Resolved.

  14. L-03 Low Token Mint Not Validated In SetWithdrawalAccount Validation Resolved
    Location
    lc_deposit.rs: 89

    Description

    The SetWithdrawalAccount context accepts a token_mint account but does not constrain it to match vault_state.token_mint. If the wrong mint is passed, the function creates an ATA for an unrelated token which can trigger reverts when calling withdraw.

    Recommendation

    Add a constraint to ensure the provided mint matches the vault's configured mint.

    Resolution

    Degensafe Team: Resolved.

  15. L-04 Low Loose ATA Existence Check Validation Partially resolved
    Location
    lc_deposit.rs: 190

    Description

    Both set_withdrawal_account and create_wallet_ata_if_needed compute the canonical ATA address but use weak checks to determine if the ATA already exists:

    • set_withdrawal_account checks if the account is owned by the Token program
    • create_wallet_ata_if_needed checks if the account is not owned by the System program

    Neither verifies that the passed associated_token account actually matches the derived canonical ATA address. Any account passing these ownership checks is treated as the valid ATA, including unrelated token accounts, other wallets' ATAs, or accounts for different mints.

    Recommendation

    Verify the passed account address matches the canonical ATA before checking existence. Explicitly handle the three cases: Token program owned (exists), System program owned (needs creation), and other (reject).

    Resolution

    Degensafe Team: Partially Resolved.

  16. L-05 Low Balance Tracking Desync With Direct Transfer Logical Error Resolved
    Location
    sol_deposit.rs: 76

    Description

    The sol_deposit and lc_deposit programs maintain a balance field in vault_state that is incremented during deposits and set to zero during withdrawals. However, the actual withdrawal transfers the full lamport balance of vault_pda, not the tracked balance value.

    If SOL/SPL token is sent directly to vault_pda outside of the deposit instruction, the vault_state.balance will not reflect the true holdings. This creates a desync between tracked and actual balances and may confuse integrators/DegenSafe’s backend component.

    Recommendation

    Consider removing the redundant balance tracking and rely solely on the PDA's lamport balance and token amount in the vault ATA.

    Resolution

    Degensafe Team: Resolved.

  17. L-06 Low ATA Address Not Validated Validation Resolved
    Location
    lc_deposit.rs: 70

    Description

    Both set_withdrawal_account and create_wallet_ata_if_needed compute the canonical ATA address using get_associated_token_address, but neither verifies that the passed associated_token account actually matches this derived address.

    A caller could pass any arbitrary account as associated_token. The function would either skip creation (if the account passes the loose existence check) or attempt to create an ATA at the wrong address.

    Recommendation

    Add a check to verify the passed associated token account matches the calculated ATA.

    Resolution

    Degensafe Team: Unresolved.

  18. L-07 Low Reward Capping For High-Decimal Tokens Logical Error Resolved
    Location
    staking.rs

    Description

    The reward calculation uses u128 for intermediate arithmetic but clamps to u64 before transfer: reward.min(u64::MAX as u128) as u64.

    For tokens with high decimals (e.g., 18 decimals), u64 can only represent ~18.44 whole tokens. If such tokens are used, rewards exceeding u64::MAX would be capped rather than failing, resulting in users receiving less than earned.

    Recommendation

    Document that this system is only intended for usage with lower decimal tokens.

    Resolution

    Degensafe Team: Unresolved.

  19. L-08 Low Order ID Length Not Validated Validation Resolved
    Location
    Global

    Description

    The deposit function in both lc_deposit and sol_deposit programs accepts an order_id parameter of type String without validating its length. The DepositRecord account allocates a fixed 64 bytes for the order ID string content, but no runtime check enforces this limit.

    When a user submits an order ID exceeding 64 bytes, the transaction fails during account serialization because the data cannot fit in the allocated space. This results in a confusing error message rather than a clear validation failure

    Recommendation

    Consider adding explicit length validation at the start of the deposit function in both programs.

    Resolution

    Degensafe Team: Unresolved.

  20. L-09 Low Backend Must Act Accordingly Warning Resolved
    Location
    Global

    Description

    The backend systems consuming the on-chain data exposed through functions such as check_deposit, get_user_stake_info, etc. must validate and interpret it correctly. Consider some of the potential risks below that depend on backend implementation:

    1. Deposit Record Reuse
    • Deposit records (DepositRecord) persist on-chain indefinitely after creation
    • If the backend does not mark deposits as "consumed" after token creation, an attacker could potentially reuse the same deposit

    proof multiple times

    • Impact: Double-spending of deposit credits, unlimited token creation from single payment
    1. Depositor Identity Verification
    • The user field in deposit records shows who made the deposit, but contracts don't enforce that the requester matches the

    depositor

    • If backend doesn't verify deposit_record.user == requesting_user, one user could claim another's deposit
    • Impact: Theft of payment credits, unauthorized token creation
    1. Timestamp/Expiry Validation
    • Deposit records include timestamp but contracts don't enforce expiry
    • If backend doesn't check deposit freshness, old deposits could be used indefinitely
    • Impact: Stale payment exploitation, accounting issues
    1. Flash-Staking Exploitation
    • last_staked_time is provided but no minimum lock period is enforced on-chain
    • If off-chain systems grant benefits (airdrops, governance, tier access) based solely on amount > 0 without checking staking

    duration, attackers can:

    • Stake immediately before snapshots
    • Qualify for benefits
    • Withdraw immediately after
    • Impact: Unfair distribution of rewards, governance manipulation
    1. Balance Desynchronization
    • vault_state.balance tracks deposits through the program, but actual on-chain balance may differ if funds are sent directly
    • If backend relies on vault_state.balance instead of actual account balance, accounting errors occur
    • Impact: Incorrect financial reporting, potential loss tracking

    Recommendation

    Ensure the backend has been appropriately reviewed as it plays a key role in the processing of on-chain data.

    Resolution

    Degensafe Team: Unresolved.

  21. I-01 Informational Unbounded Reward Percentage Validation Resolved
    Location
    staking.rs: 100

    Description

    The update_reward_percentage function accepts any u64 value without validation. An admin can set reward_percentage to an arbitrarily high value, causing the reward calculation to produce extreme payouts.

    Recommendation

    Consider enforcing validation for the reward percentage on-chain.

    Resolution

    Degensafe Team: Resolved.

  22. I-02 Informational Unused DepositNotFound Error Best Practices Resolved
    Location
    sol_deposit.rs: 247

    Description

    The VaultError enum defines a DepositNotFound variant that is never used anywhere in the program logic.

    Recommendation

    Remove the unused error variant to reduce code clutter, or implement it in check_deposit if the intent was to return a custom error for missing records.

    Resolution

    Degensafe Team: Resolved.

  23. I-03 Informational First Deposit Requires Rent-Exempt Amount Warning Resolved
    Location
    sol_deposit.rs: 30

    Description

    The vault_pda account is declared as an unchecked AccountInfo with no initialization logic. On the first deposit, this PDA does not exist on-chain and holds zero lamports.

    The System Program's transfer instruction will fail if the destination account would fall below the rent-exempt minimum after the transfer, effectively requiring the first depositor to send at least the rent-exempt amount to bootstrap the PDA.

    Hence the first deposit actually has a hidden minimum that the amount > 0 validation does not encompass, and that subsequent deposits do not have to follow.

    Recommendation

    Explicitly initialize the vault_pda and/or document this behavior clearly.

    Resolution

    Degensafe Team: Unresolved.

  24. I-04 Informational Missing PDA Seed Validation On Pool Account Warning Partially resolved
    Location
    staking.rs

    Description

    Multiple instruction account structs in the staking program lack PDA seed validation on the pool account.

    While CreatePool correctly derives the pool PDA using seeds [b"staking_pool", token_mint.key().as_ref()], subsequent instructions only use #[account(mut)] without verifying the pool address matches the expected PDA derivation.

    Note that impact is limited since Pool is a typed Account<'info, Pool>, hence Anchor already enforces program ownership.

    Structs missing seeds constraint on pool:

    • GetPoolInfo - read-only, no owner check
    • UpdateRewardMint - runtime owner check
    • DepositReward - runtime owner check
    • GetUserStakeInfo - read-only, no owner check
    • WithdrawStake - no owner check
    • WithdrawReward - runtime owner check
    • UpdateRewardPercentage - runtime owner check
    • SetStakingActive - runtime owner check
    • DepositStake - no owner check

    Recommendation

    Consider adding the seeds for the pool account in the various structs.

    Resolution

    Degensafe Team: Partially Resolved.

  25. I-05 Informational Reward Claiming Requires Zero Amount Withdrawal Logical Error Resolved
    Location
    staking.rs: 245

    Description

    To claim accumulated rewards without unstaking principal, users must call withdraw_stake(0). This may be unintuitive for users and should be clearly documented.

    Recommendation

    Clearly document this behavior.

    Resolution

    Degensafe Team: Resolved.

  26. I-06 Informational Single Pool Per Token Mint Limitation Documentation Resolved
    Location
    staking.rs

    Description

    The pool PDA is derived using seeds [b"staking_pool", token_mint.key().as_ref()], which means only one staking pool can exist per token mint. This is a design constraint that prevents creating multiple pools with different reward configurations for the same token.

    Recommendation

    Clearly document this constraint.

    Resolution

    Degensafe Team: Resolved.

  27. I-07 Informational Inconsistent ATA Existence Check Warning Partially resolved
    Location
    lc_deposit.rs: 204

    Description

    The codebase uses two different methods to determine whether an Associated Token Account (ATA) already exists.

    In set_withdrawal_account, the check verifies ownership by the Token Program specifically, while create_wallet_ata_if_needed uses a looser check that assumes any account not owned by the System Program is an existing ATA.

    The loose check would incorrectly skip ATA creation if the account were owned by any program other than System, not just the Token Program.

    Recommendation

    Standardize on the stricter ownership check (verifying owner == token::ID) and consider validating the generated ata address against the passed account if exists.

    Resolution

    Degensafe Team: Partially Resolved.

More from Degen Safe

  1. Contract Updates

    17 findings 17 findings: 1 medium, 5 low, 11 informational

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.

Get a quote