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

Security review · March 2026

Contract Updates

for Degen Safe

Degen Safe engaged Guardian to review the security of its Contract Updates. From the 03/09/2026 to the 03/12/2026, a team of 2 auditors reviewed the source code in scope.

Published
Rounds
Main Review, Remediation Review
Language
Rust
Chains
Solana
Sector
Token launches, Staking
  • 0 Critical
  • 0 High
  • 1 Medium
  • 5 Low
  • 11 Informational

13 resolved · 1 partially resolved · 3 acknowledged

Scope

Overview

Degen Safe engaged Guardian to review the security of its Contract Updates. From the 03/09/2026 to the 03/12/2026, a team of 2 auditors reviewed the source code in scope.

Findings 17

Main Review

13 findings
  1. M-01 Medium Changing Reward Token Causes Loss Of Rewards Logical Error Resolved
    Location
    lib.rs: 291-294
    Round
    Main Review

    Description

    Checking total_staked == 0 before allowing a reward mint swap, if nobody has staked, should be safe. The problem is that total_staked only tracks active deposits. It ignores user_stake.unclaimed.

    A user can withdraw their full stake while the reward vault is empty. When that happens, the code stores all owed rewards into user_stake.unclaimed and decrements pool.total_staked to 0. The pool now passes the guard, but there's still a user owed rewards in the old mint.

    Once the admin swaps the mint, claim_reward pays from the new vault. The user's unclaimed, denominated in the old token, gets paid in the new token. If the tokens have different decimals or value, the user either loses money or gets overpaid.

    Example: User has 50 USDC (6 decimals) unclaimed. Admin swaps to a 9-decimal token. User claims and gets 50_000_000 raw units of the new token = 0.05 tokens instead of 50.

    Recommendation

    The guard needs to also check that no user has outstanding unclaimed rewards.

    Resolution

    Degen Safe: Resolved.

  2. L-01 Low Locked Rewards In Old Vault After Mint Update Validation Resolved
    Location
    lib.rs: 885-893
    Round
    Main Review

    Description

    update_reward_mint allows the admin to update the reward token, and this instruction updates the reward_vaultt based on the new token.

    The admin can withdraw reward tokens from the reward vault. However, withdrawals are only supported for the current reward vault. If the reward mint is updated while some rewards from the previous token remain unclaimed, there is no mechanism to withdraw those remaining tokens from the previous reward vault.

    Recommendation

    Consider allowing a reward mint update only after all rewards from the previous mint have been claimed and/or withdrawn. Alternatively, allow the admin to withdraw tokens from previous reward_vaults.

    Resolution

    Degen Safe: Resolved.

  3. L-02 Low No Duplicate Check In Update_reward_percentage Validation Resolved
    Location
    lib.rs: 315
    Round
    Main Review

    Description

    The update_reward_percentage instruction does not check whether the new reward rate is equal to the current reward rate. As a result, the reward rate can be updated to the same value. While this does not affect reward calculations, it unnecessarily creates an additional epoch, which is subject to a maximum limit.

    Recommendation

    Add a check in the instruction to ensure the new reward rate differs from the current rate.

    Resolution

    Degen Safe: Resolved.

  4. L-03 Low Rent On User PDAs Not Reclaimable On Withdraw Validation Resolved
    Location
    spl_token_vault_program/src/lib.rs, sol_vault_program/src/lib.rs, stake_program/src/lib.rs
    Round
    Main Review

    Description

    For each of the below accounts:

    • stake_program: user_stake PDA (DepositStake, init_if_needed, payer = user).
    • spl_token_vault_program: deposit_record PDA (Deposit, init, payer = user).
    • sol_vault_program: deposit_record PDA (Deposit, init, payer = depositor).

    the user (or depositor) pays rent at creation. The programs never close these accounts: there is no close = ... constraint and no instruction that closes them. So after a user fully withdraws stake (stake program) or the deposit record is no longer needed (vault programs), the rent lamports remain locked in the PDA forever. Users cannot reclaim that rent, and the protocol does not redirect it to any beneficiary.

    Recommendation

    Consider adding an instruction that closes the account when it is safe, or documenting that rent is non-recoverable so users and integrators can account for it.

    Resolution

    Degen Safe: Resolved.

  5. L-04 Low Reward Mint Check Bypassed On Update Logical Error Resolved
    Location
    lib.rs: 155-158
    Round
    Main Review

    Description

    At pool creation, create_pool enforces that the reward mint must match the staking token mint:

    require!(
        ctx.accounts.token_mint.key() == ctx.accounts.reward_mint.key(),
        CustomError::RewardMintMustMatchStakeMint
    );
    

    The comment above it (line 158) explains the rationale:

    "Enforce same-token staking: reward mint must be the same as the staking token mint. This eliminates decimal mismatch issues and simplifies reward calculations."

    However, the update_reward_mint function performs no such check.

    Recommendation

    Consider either allowing to have any token, or remove the update_reward_mint function.

    Resolution

    Degen Safe: Resolved.

  6. I-01 Informational Notes Regarding Fee-On-Transfer Support Informational Resolved
    Location
    lib.rs: 537-542
    Round
    Main Review

    Description

    The deposit flow in spl-token-vault records the actual amount of tokens received to support fee-on-transfer tokens. In contrast, the deposit flow in the staking program does not perform this check and accepts the provided amount without verification. As a result, the staking program does not support fee-on-transfer tokens in its current form.

    This creates a discrepancy between the spl-token-vault and staking programs regarding support for fee-on-transfer tokens.

    Additionally, the current programs do not support Token-2022 and only interact with the legacy SPL Token program. Since the legacy token program does not implement hooks or fee-on-transfer logic, the before/after balance check becomes redundant when using legacy tokens, even though the comments state fee-on-transfer token support.

    Recommendation

    Document this behavior if it is intended. Alternatively, consider aligning the behavior between the programs. If fee-on-transfer token support is necessary, consider using Token-2022.

    Resolution

    Degen Safe: Resolved.

  7. I-02 Informational Unused Errors Error Resolved
    Location
    stake_program,spl_token_vault
    Round
    Main Review

    Description

    Six error variants are defined but never referenced anywhere in the code.

    stake_program (lib.rs)

    DecimalMismatch

    InvalidProgramData

    InitializeConfig

    spl_token_vault (lib.rs)

    Unauthorized

    NotRentExempt

    InvalidDataLength

    CorruptedTokenAccount

    Recommendation

    Consider removing all unused errors.

    Resolution

    Degen Safe: Resolved.

  8. I-03 Informational No Guard On Reward Deposit Or Withdrawal Rewards Partially resolved
    Location
    stake_program/src/lib.rs
    Round
    Main Review

    Description

    withdraw_reward lets the admin pull any amount from the reward vault with no cap. There's nothing stopping them from withdrawing tokens that are already owed to stakers, the function doesn't track how much of the vault is committed.

    On the flip side, there's also no requirement for the admin to deposit enough rewards to cover what stakers have accrued. If the vault is underfunded, claim_reward reverts and withdraw_stake silently pays 0 rewards.

    Both are admin responsibility, but the program doesn't enforce either.

    Recommendation

    Track committed rewards at pool level and cap admin withdrawals to the uncommitted balance or consider documenting this behavior for users.

    Resolution

    Degen Safe: Partially Resolved.

  9. I-04 Informational Deprecated Rent Sysvar Passed As Account Best Practices Resolved
    Location
    stake_program/src/lib.rs , spl_token_vault_program/src/lib.rs
    Round
    Main Review

    Description

    Sysvar<'info, Rent> is deprecated by Solana. The runtime still supports it but the recommended approach is Rent::get().

    sol-vault already does this, but stake and spl-token-vault still pass it as an account.

    Recommendation

    Remove pub rent: Sysvar<'info, Rent> from all account structs and use Rent::get().

    Resolution

    Degen Safe: Resolved.

  10. I-05 Informational Missing Events Events Resolved
    Location
    stake_program/src/lib.rs
    Round
    Main Review

    Description

    Three state-changing functions only use msg!() logs but don't emit typed events:

    • initialize_config — sets the global admin
    • transfer_admin — transfers global admin to a new address
    • update_pool_authority — changes pool owner

    All other state-changing functions (create_pool, deposit_stake, withdraw_stake, claim_reward, etc.) properly emit typed events. These three are the only ones that don't.

    Recommendation

    Add typed events for all three, e.g. ConfigInitializedEvent, AdminTransferredEvent, PoolAuthorityUpdatedEvent.

    Resolution

    Degen Safe: Resolved.

  11. I-06 Informational No Cancel Path For Deposits And Trust Assumptions Trust Assumptions Acknowledged
    Location
    Global
    Round
    Main Review

    Description

    Both sol-vault and spl-token-vault expose only a deposit(order_id, amount) instruction for users. Deposited funds move into PDAs fully controlled by the program, and the only withdrawal instruction can be invoked by the vault authority to sweep all funds to a preconfigured wallet. No user-facing cancel/withdraw path exists, and deposit records are read-only receipts.

    Users must rely entirely on the operator’s off-chain infrastructure to acknowledge deposits, fulfill services, or process refunds. A failure or malicious action off-chain leaves deposits permanently locked or lost upon authority withdrawal.

    Recommendation

    This issue is intended to inform users, and no change is required as this is a design choice. Consider documenting these behaviors and expected workflows.

    Resolution

    Degen Safe: Acknowledged.

  12. I-07 Informational Consider Using is_on_curve Check For Updates Informational Acknowledged
    Location
    Global
    Round
    Main Review

    Description

    set_withdrawal_account, update_authority, transfer_admin, and update_pool_authority instructions only deny a small set of obviously invalid pubkeys and do not verify that the new authority/account is compatible with the program’s wallet-based authorization model. Since these roles are expected to be controlled by ordinary signer wallets, assigning an off-curve address such as a PDA, or another non-signable address class, can render future authority-gated operations unusable.

    While it is impossible to check and deny all possible unusable addresses, it is recommended to use Pubkey::is_on_curve() to at least ensure the address corresponds to a valid Ed25519 point.

    Recommendation

    Consider validating new addresses using Pubkey::is_on_curve() when updating addresses or wallets.

    Resolution

    Degen Safe: Acknowledged.

  13. I-08 Informational Warning Related To Offchain Part Warning Acknowledged
    Location
    Global
    Round
    Main Review

    Description

    Users are expected to interact with the protocol via the backend, and order_ids are provided by the backend during deposits. However, users can deposit directly on-chain and provide arbitrary order_ids. In such cases, there is no guarantee that these deposits will be accounted for or refunded manually. Users must be made aware of this behavior and warned accordingly. They should not interact with the protocol directly on-chain.

    Additionally, backend validation must account for arbitrarily supplied on-chain order_ids and ensure it does not reuse an already-used order_id if IDs are generated via simple incrementation.

    Recommendation

    Inform and warn users not to perform direct deposits, document the protocol's expected behavior, and ensure that backend validation accounts for this case.

    Resolution

    Degen Safe: Acknowledged.

Remediation Review

4 findings
  1. L-01 Low Incorrect Committed_rewards Accounting Allows Draining Of Rewards Logical Error Resolved
    Location
    lib.rs: 655
    Round
    Remediation Review

    Description

    In withdraw_stake, when rewards get paid out (reward_to_send > 0), both prev_unclaimed and pending are subtracted from pool.committed_rewards:

    pool.committed_rewards = pool.committed_rewards
        .saturating_sub(prev_unclaimed)
        .saturating_sub(pending);
    

    The problem is that pending, freshly calculated by calculate_pending_reward, was never added to committed_rewards to begin with. committed_rewards only grows when a user interaction materializes rewards (via deposit_stake, or the else branch of withdraw_stake when rewards stay as unclaimed). So subtracting pending here removes value that was never accounted for, deflating the counter.

    Since saturating_sub clamps to 0 instead of reverting, the transaction goes through without error. The pool's accounting just gets quietly corrupted.

    This feeds directly into let uncommitted = reward_vault.amount.saturating_sub(pool.committed_rewards);withdraw_reward, where the admin's withdrawable balance is:

    Every time a user withdraws stake and gets rewards paid, committed_rewards drops further than it should. uncommitted grows, and the admin can pull out tokens that are owed to other stakers. After enough withdrawals the counter can hit 0, leaving the entire vault exposed.

    Recommendation

    Only subtract prev_unclaimed when rewards are paid out, pending was never added so it should not be subtracted.

    Resolution

    Degen Safe: Resolved.

  2. I-01 Informational Misplaced Comment Informational Resolved
    Location
    lib.rs: 315-316
    Round
    Remediation Review

    Description

    After adding the close_deposit_record instruction in the spl-token-vault, the comment related to the update_authority instruction remained above the newly added instruction.

        /// Transfer vault authority to a new address.
        /// Validates the new authority is not a reserved address.
        /// Close a deposit record and return rent to the depositor.
        /// Only the original depositor can close their own record.
        pub fn close_deposit_record(_ctx: Context<CloseDepositRecord>, _order_id: String) ->
    Result<()> {
    

    Recommendation

    Update the comment.

    Resolution

    Degen Safe: Resolved.

  3. I-02 Informational Notes Related To Committed Rewards Informational Resolved
    Location
    stake_program/src/lib.rs
    Round
    Remediation Review

    Description

    The committed_rewards field is introduced to track rewards earned by users and to prevent the admin from withdrawing them. However, it only accounts for "unclaimed" rewards and does not include "pending" rewards.

    If a user stakes and remains idle for a long time, their rewards continue to accrue as "pending", but are not recorded as "unclaimed" until they interact with their stake (e.g., deposit, withdraw, or claim). As a result, these rewards are not included in committed_rewards

    It is not feasible to track all users’ pending rewards across the codebase, as doing so would require iterating over all users or a significant architectural change. However, it is important to note that pending rewards are not protected and can still be withdrawn.

    Recommendation

    Document this behavior as a design choice and inform users that pending rewards may be withdrawn. Stakers should periodically update their stake or claim if they want their rewards to be protected.

    Optionally, introduce a guard such as: ‘if pool.total_staked > 0, only allow withdrawals above a defined safety buffer.’

    Resolution

    Degen Safe: Resolved.

  4. I-03 Informational Warning Regarding Deposit Record Closure Warning Resolved
    Location
    lib.rs: 319
    Round
    Remediation Review

    Description

    Users can close their deposit records to reclaim the rent. The CloseDepositRecord instruction ensures that only the original depositor can close the record.

    However, there are no additional restrictions on when a deposit record may be closed. A user may close it even before the backend or off-chain components of the protocol have processed or fulfilled that deposit. Once closed, the on-chain deposit record is deleted and can no longer be queried through check_deposit.

    Users should be clearly warned not to close a deposit record before the backend has fully processed it, as doing so removes the on-chain record and may interfere with verification or reconciliation of that deposit.

    Recommendation

    Document this behavior and warn users about the deposit record closures.

    Resolution

    Degen Safe: Resolved.

More from Degen Safe

  1. Protocol Review

    27 findings5 critical · 2 high 27 findings: 5 critical, 2 high, 4 medium, 9 low, 7 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