Blueprint: Glow Assessment - Blueprint Finance


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/17/2025

Date of Engagement: December 13th, 2024 - February 28th, 2025

Summary

100% of all REPORTED Findings have been addressed

All findings

16

Critical

0

High

0

Medium

1

Low

7

Informational

8


1. Introduction

Blueprint Finance engaged Halborn to conduct a security assessment on their Glow V1 and Lookup Table Registry Solana programs beginning on December 13th, 2024, and ending on February 28th, 2025. The security assessment was scoped to the Solana Programs provided in glow-v1 and lookup-table-registry GitHub repositories. Commit hashes and further details can be found in the Scope section of this report.


The Glow V1 program is a noncustodial borrowing and lending protocol consisting of four interconnected programs:

    • Airspace – Implements a permission system that isolates markets and controls which programs users in those markets can interact with.

    • Metadata – Manages program metadata, including enabled trading/deposit tokens and oracle information.

    • Margin – Allows users to interact with other programs using lower collateral than required for direct interaction.

    • Margin-Pool – Implements a variable-rate lending and borrowing pools.


The Lookup Table Registry is a program for creating and tracking address lookup tables.


During the security assessment, the Glow team implemented two code updates:

    • Removed the control program previously used for bootstrapping and configuring margin pools.

    • Added support for the pull-based Pyth oracle architecture.

2. Assessment Summary

Halborn was provided 11 weeks for the engagement and assigned one full-time security engineer to review the security of the Solana Programs in scope. The engineer is a blockchain and smart contract security expert with advanced smart contract hacking skills, and deep knowledge of multiple blockchain protocols.

The purpose of the assessment is to:

    • Identify potential security issues within the Solana Programs.

    • Ensure that smart contract functionality operates as intended.

In summary, Halborn identified some security concerns that were mostly addressed by the Glow team. The main ones were the following:

    • Verify that registry accounts can be closed only by a dedicated authority.

    • Implement two step authority transfer.

    • Make sure the administrator cannot transfer positions to different user.

    • Properly verify instruction parameters.

    • Validate mint extensions to avoid potentially dangerous extensions.

    • Make sure the liquidator are incentivized to restore accounts to healthy state.

    • Prevent the airspace authorities to manipulate arbitrary metadata accounts.

    • Optimize reallocations by ensuring new memory is not zeroed twice.

3. Test Approach and Methodology

Halborn performed a combination of a manual review of the source code and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of the program assessment. While manual testing is recommended to uncover flaws in business logic, processes, and implementation; automated testing techniques help enhance coverage of programs and can quickly identify items that do not follow security best practices.

The following phases and associated tools were used throughout the term of the assessment:

- Research into the architecture, purpose, and use of the platform.

- Manual program source code review to identify business logic issues.

- Mapping out possible attack vectors

- Thorough assessment of safety and usage of critical Rust variables and functions in scope that could lead to arithmetic vulnerabilities.

- Scanning dependencies for known vulnerabilities (`cargo audit`).

- Local runtime testing (`solana-test-framework`)

4. RISK METHODOLOGY

Every vulnerability and issue observed by Halborn is ranked based on two sets of Metrics and a Severity Coefficient. This system is inspired by the industry standard Common Vulnerability Scoring System.
The two Metric sets are: Exploitability and Impact. Exploitability captures the ease and technical means by which vulnerabilities can be exploited and Impact describes the consequences of a successful exploit.
The Severity Coefficients is designed to further refine the accuracy of the ranking with two factors: Reversibility and Scope. These capture the impact of the vulnerability on the environment as well as the number of users and smart contracts affected.
The final score is a value between 0-10 rounded up to 1 decimal place and 10 corresponding to the highest security risk. This provides an objective and accurate rating of the severity of security vulnerabilities in smart contracts.
The system is designed to assist in identifying and prioritizing vulnerabilities based on their level of risk to address the most critical issues in a timely manner.

4.1 EXPLOITABILITY

Attack Origin (AO):
Captures whether the attack requires compromising a specific account.
Attack Cost (AC):
Captures the cost of exploiting the vulnerability incurred by the attacker relative to sending a single transaction on the relevant blockchain. Includes but is not limited to financial and computational cost.
Attack Complexity (AX):
Describes the conditions beyond the attacker’s control that must exist in order to exploit the vulnerability. Includes but is not limited to macro situation, available third-party liquidity and regulatory challenges.
Metrics:
EXPLOITABILITY METRIC (mem_e)METRIC VALUENUMERICAL VALUE
Attack Origin (AO)Arbitrary (AO:A)
Specific (AO:S)
1
0.2
Attack Cost (AC)Low (AC:L)
Medium (AC:M)
High (AC:H)
1
0.67
0.33
Attack Complexity (AX)Low (AX:L)
Medium (AX:M)
High (AX:H)
1
0.67
0.33
Exploitability EE is calculated using the following formula:

E=meE = \prod m_e

4.2 IMPACT

Confidentiality (C):
Measures the impact to the confidentiality of the information resources managed by the contract due to a successfully exploited vulnerability. Confidentiality refers to limiting access to authorized users only.
Integrity (I):
Measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of data stored and/or processed on-chain. Integrity impact directly affecting Deposit or Yield records is excluded.
Availability (A):
Measures the impact to the availability of the impacted component resulting from a successfully exploited vulnerability. This metric refers to smart contract features and functionality, not state. Availability impact directly affecting Deposit or Yield is excluded.
Deposit (D):
Measures the impact to the deposits made to the contract by either users or owners.
Yield (Y):
Measures the impact to the yield generated by the contract for either users or owners.
Metrics:
IMPACT METRIC (mIm_I)METRIC VALUENUMERICAL VALUE
Confidentiality (C)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Integrity (I)None (I:N)
Low (I:L)
Medium (I:M)
High (I:H)
Critical (I:C)
0
0.25
0.5
0.75
1
Availability (A)None (A:N)
Low (A:L)
Medium (A:M)
High (A:H)
Critical (A:C)
0
0.25
0.5
0.75
1
Deposit (D)None (D:N)
Low (D:L)
Medium (D:M)
High (D:H)
Critical (D:C)
0
0.25
0.5
0.75
1
Yield (Y)None (Y:N)
Low (Y:L)
Medium (Y:M)
High (Y:H)
Critical (Y:C)
0
0.25
0.5
0.75
1
Impact II is calculated using the following formula:

I=max(mI)+mImax(mI)4I = max(m_I) + \frac{\sum{m_I} - max(m_I)}{4}

4.3 SEVERITY COEFFICIENT

Reversibility (R):
Describes the share of the exploited vulnerability effects that can be reversed. For upgradeable contracts, assume the contract private key is available.
Scope (S):
Captures whether a vulnerability in one vulnerable contract impacts resources in other contracts.
Metrics:
SEVERITY COEFFICIENT (CC)COEFFICIENT VALUENUMERICAL VALUE
Reversibility (rr)None (R:N)
Partial (R:P)
Full (R:F)
1
0.5
0.25
Scope (ss)Changed (S:C)
Unchanged (S:U)
1.25
1
Severity Coefficient CC is obtained by the following product:

C=rsC = rs

The Vulnerability Severity Score SS is obtained by:

S=min(10,EIC10)S = min(10, EIC * 10)

The score is rounded up to 1 decimal places.
SeverityScore Value Range
Critical9 - 10
High7 - 8.9
Medium4.5 - 6.9
Low2 - 4.4
Informational0 - 1.9

5. SCOPE

Files and Repository
(a) Repository: glow-v1
(b) Assessed Commit ID: 47367ed
(c) Items in scope:
  • ./programs/margin-pool/Cargo.toml
  • ./programs/margin-pool/Xargo.toml
  • ./programs/margin-pool/src/instructions/margin_refresh_position.rs
↓ Expand ↓
Out-of-Scope: ./programs/test-service/Cargo.toml, ./programs/test-service/Xargo.toml, ./programs/test-service/src/instructions/slippy/mod.rs, ./programs/test-service/src/instructions/mod.rs, ./programs/test-service/src/instructions/if_not_initialized.rs, ./programs/test-service/src/instructions/tokens/token_update_pyth_price.rs, ./programs/test-service/src/instructions/tokens/token_register.rs, ./programs/test-service/src/instructions/tokens/token_create.rs, ./programs/test-service/src/instructions/tokens/mod.rs, ./programs/test-service/src/instructions/tokens/token_init_native.rs, ./programs/test-service/src/instructions/tokens/token_request.rs, ./programs/test-service/src/util.rs, ./programs/test-service/src/error.rs, ./programs/test-service/src/lib.rs, ./programs/test-service/src/state/tokens.rs, ./programs/test-service/src/state/mod.rs, ./programs/test-service/src/state/slippy.rs, ./programs/turbo.json, ./programs/package.json, ./libraries/rust/tools/Cargo.toml, ./libraries/rust/tools/src/lookup_tables.rs, ./libraries/rust/tools/src/lib.rs, ./libraries/rust/solana-client/Cargo.toml, ./libraries/rust/solana-client/src/lookup_tables.rs, ./libraries/rust/solana-client/src/transaction.rs, ./libraries/rust/solana-client/src/util/pubkey.rs, ./libraries/rust/solana-client/src/util/mod.rs, ./libraries/rust/solana-client/src/util/data.rs, ./libraries/rust/solana-client/src/util/keypair.rs, ./libraries/rust/solana-client/src/lib.rs, ./libraries/rust/solana-client/src/signature.rs, ./libraries/rust/solana-client/src/rpc/native.rs, ./libraries/rust/solana-client/src/network.rs, ./libraries/rust/solana-client/src/rpc.rs, ./libraries/rust/client-web/Cargo.toml, ./libraries/rust/client-web/src/margin.rs, ./libraries/rust/client-web/src/solana_web3.rs, ./libraries/rust/client-web/src/error.rs, ./libraries/rust/client-web/src/lib.rs, ./libraries/rust/client-web/src/margin_pool.rs, ./libraries/rust/client-web/src/wallet.rs, ./libraries/rust/simulation/Cargo.toml, ./libraries/rust/simulation/src/runtime.rs, ./libraries/rust/simulation/src/lib.rs, ./libraries/rust/simulation/src/solana_rpc_api.rs, ./libraries/rust/testing/Cargo.toml, ./libraries/rust/testing/src/lib.rs, ./libraries/rust/environment/Cargo.toml, ./libraries/rust/environment/src/lookup_tables.rs, ./libraries/rust/environment/src/config.rs, ./libraries/rust/environment/src/lib.rs, ./libraries/rust/environment/src/builder/margin.rs, ./libraries/rust/environment/src/builder/global.rs, ./libraries/rust/environment/src/builder/margin_pool.rs, ./libraries/rust/environment/src/client_config.rs, ./libraries/rust/environment/src/builder.rs, ./libraries/rust/margin/Cargo.toml, ./libraries/rust/margin/src/margin_account_ext.rs, ./libraries/rust/margin/src/ix_builder.rs, ./libraries/rust/margin/src/lookup_tables.rs, ./libraries/rust/margin/src/tokens.rs, ./libraries/rust/margin/src/util/queue_processor.rs, ./libraries/rust/margin/src/util/asynchronous.rs, ./libraries/rust/margin/src/util/no_dupe_queue.rs, ./libraries/rust/margin/src/util/mod.rs, ./libraries/rust/margin/src/refresh/position_refresher.rs, ./libraries/rust/margin/src/refresh/deposit.rs, ./libraries/rust/margin/src/refresh/mod.rs, ./libraries/rust/margin/src/refresh/pool.rs, ./libraries/rust/margin/src/lib.rs, ./libraries/rust/margin/src/test_service.rs, ./libraries/rust/margin/src/solana/transaction.rs, ./libraries/rust/margin/src/solana/mod.rs, ./libraries/rust/margin/src/tx_builder/airspace.rs, ./libraries/rust/margin/src/tx_builder/invoke_pool.rs, ./libraries/rust/margin/src/tx_builder/user.rs, ./libraries/rust/margin/src/tx_builder/invoke_context.rs, ./libraries/rust/margin/src/tx_builder/mod.rs, ./libraries/rust/margin/src/get_state/rpc_query.rs, ./libraries/rust/margin/src/get_state/margin_state.rs, ./libraries/rust/margin/src/get_state/mod.rs, ./libraries/rust/margin/src/margin_integrator.rs, ./libraries/rust/static-program-registry/Cargo.toml, ./libraries/rust/static-program-registry/src/lib.rs, ./libraries/rust/client/Cargo.toml, ./libraries/rust/client/src/margin.rs, ./libraries/rust/client/src/client.rs, ./libraries/rust/client/src/config.rs, ./libraries/rust/client/src/lib.rs, ./libraries/rust/client/src/test_service.rs, ./libraries/rust/client/src/margin_pool.rs, ./libraries/rust/client/src/state/lookup_tables.rs, ./libraries/rust/client/src/state/margin.rs, ./libraries/rust/client/src/state/tokens.rs, ./libraries/rust/client/src/state/oracles.rs, ./libraries/rust/client/src/state/margin_pool.rs, ./libraries/rust/client/src/wallet.rs, ./libraries/rust/client/src/state.rs, ./libraries/rust/client/src/fixed_term/util.rs, third party dependencies and economic attacks.
Files and Repository
(a) Repository: lookup-table-registry
(b) Assessed Commit ID: 923f5a3
(c) Items in scope:
  • ./programs/lookup-table-registry/Cargo.toml
  • ./programs/lookup-table-registry/Xargo.toml
  • ./programs/lookup-table-registry/src/lib.rs
↓ Expand ↓
Out-of-Scope: ./rust-toolchain, ./LICENSE, ./Dockerfile, ./tests/lookup-table-registry.ts, ./Cargo.lock, ./libraries/rust/Cargo.toml, ./libraries/rust/src/instructions.rs, ./libraries/rust/src/lib.rs, ./libraries/rust/src/writer.rs, ./libraries/rust/src/reader.rs, ./libraries/rust/src/common.rs, ./libraries/solana-address-lookup-table-program-gateway/Cargo.toml, ./libraries/solana-address-lookup-table-program-gateway/src/lib.rs, ./libraries/solana-address-lookup-table-program-gateway/src/stub/state.rs, ./libraries/solana-address-lookup-table-program-gateway/src/stub/instruction.rs, ./server/Cargo.toml, ./server/src/main.rs, ./.prettierignore, ./README.md, ./test-validator, ./.env.template, ./yarn.lock, ./.dockerignore, ./.gitignore, ./package.json, ./.github/workflows/build.yaml, ./tsconfig.json, third party dependencies and economic attacks.
Files and Repository
(a) Repository: glow-v1
(c) Items in scope:
  • ./programs/margin-pool/Cargo.toml
  • ./programs/margin-pool/Xargo.toml
  • ./programs/margin-pool/src/instructions/margin_refresh_position.rs
↓ Expand ↓
Out-of-Scope: ./programs/test-service/Cargo.toml, ./programs/test-service/Xargo.toml, ./programs/test-service/src/instructions/slippy/mod.rs, ./programs/test-service/src/instructions/mod.rs, ./programs/test-service/src/instructions/if_not_initialized.rs, ./programs/test-service/src/instructions/tokens/token_update_pyth_price.rs, ./programs/test-service/src/instructions/tokens/token_register.rs, ./programs/test-service/src/instructions/tokens/token_create.rs, ./programs/test-service/src/instructions/tokens/mod.rs, ./programs/test-service/src/instructions/tokens/token_init_native.rs, ./programs/test-service/src/instructions/tokens/token_request.rs, ./programs/test-service/src/util.rs, ./programs/test-service/src/error.rs, ./programs/test-service/src/lib.rs, ./programs/test-service/src/state/tokens.rs, ./programs/test-service/src/state/mod.rs, ./programs/test-service/src/state/slippy.rs, ./programs/turbo.json, ./programs/package.json, ./libraries/rust/tools/Cargo.toml, ./libraries/rust/tools/src/lookup_tables.rs, ./libraries/rust/tools/src/lib.rs, ./libraries/rust/solana-client/Cargo.toml, ./libraries/rust/solana-client/src/lookup_tables.rs, ./libraries/rust/solana-client/src/transaction.rs, ./libraries/rust/solana-client/src/util/pubkey.rs, ./libraries/rust/solana-client/src/util/mod.rs, ./libraries/rust/solana-client/src/util/data.rs, ./libraries/rust/solana-client/src/util/keypair.rs, ./libraries/rust/solana-client/src/lib.rs, ./libraries/rust/solana-client/src/signature.rs, ./libraries/rust/solana-client/src/rpc/native.rs, ./libraries/rust/solana-client/src/network.rs, ./libraries/rust/solana-client/src/rpc.rs, ./libraries/rust/client-web/Cargo.toml, ./libraries/rust/client-web/src/margin.rs, ./libraries/rust/client-web/src/solana_web3.rs, ./libraries/rust/client-web/src/error.rs, ./libraries/rust/client-web/src/lib.rs, ./libraries/rust/client-web/src/margin_pool.rs, ./libraries/rust/client-web/src/wallet.rs, ./libraries/rust/simulation/Cargo.toml, ./libraries/rust/simulation/src/runtime.rs, ./libraries/rust/simulation/src/lib.rs, ./libraries/rust/simulation/src/solana_rpc_api.rs, ./libraries/rust/testing/Cargo.toml, ./libraries/rust/testing/src/lib.rs, ./libraries/rust/environment/Cargo.toml, ./libraries/rust/environment/src/lookup_tables.rs, ./libraries/rust/environment/src/config.rs, ./libraries/rust/environment/src/lib.rs, ./libraries/rust/environment/src/builder/margin.rs, ./libraries/rust/environment/src/builder/global.rs, ./libraries/rust/environment/src/builder/margin_pool.rs, ./libraries/rust/environment/src/client_config.rs, ./libraries/rust/environment/src/builder.rs, ./libraries/rust/margin/Cargo.toml, ./libraries/rust/margin/src/margin_account_ext.rs, ./libraries/rust/margin/src/ix_builder.rs, ./libraries/rust/margin/src/lookup_tables.rs, ./libraries/rust/margin/src/tokens.rs, ./libraries/rust/margin/src/util/queue_processor.rs, ./libraries/rust/margin/src/util/asynchronous.rs, ./libraries/rust/margin/src/util/no_dupe_queue.rs, ./libraries/rust/margin/src/util/mod.rs, ./libraries/rust/margin/src/refresh/position_refresher.rs, ./libraries/rust/margin/src/refresh/deposit.rs, ./libraries/rust/margin/src/refresh/mod.rs, ./libraries/rust/margin/src/refresh/pool.rs, ./libraries/rust/margin/src/lib.rs, ./libraries/rust/margin/src/test_service.rs, ./libraries/rust/margin/src/solana/transaction.rs, ./libraries/rust/margin/src/solana/mod.rs, ./libraries/rust/margin/src/tx_builder/airspace.rs, ./libraries/rust/margin/src/tx_builder/invoke_pool.rs, ./libraries/rust/margin/src/tx_builder/user.rs, ./libraries/rust/margin/src/tx_builder/invoke_context.rs, ./libraries/rust/margin/src/tx_builder/mod.rs, ./libraries/rust/margin/src/get_state/rpc_query.rs, ./libraries/rust/margin/src/get_state/margin_state.rs, ./libraries/rust/margin/src/get_state/mod.rs, ./libraries/rust/margin/src/margin_integrator.rs, ./libraries/rust/static-program-registry/Cargo.toml, ./libraries/rust/static-program-registry/src/lib.rs, ./libraries/rust/client/Cargo.toml, ./libraries/rust/client/src/margin.rs, ./libraries/rust/client/src/client.rs, ./libraries/rust/client/src/config.rs, ./libraries/rust/client/src/lib.rs, ./libraries/rust/client/src/test_service.rs, ./libraries/rust/client/src/margin_pool.rs, ./libraries/rust/client/src/state/lookup_tables.rs, ./libraries/rust/client/src/state/margin.rs, ./libraries/rust/client/src/state/tokens.rs, ./libraries/rust/client/src/state/oracles.rs, ./libraries/rust/client/src/state/margin_pool.rs, ./libraries/rust/client/src/wallet.rs, ./libraries/rust/client/src/state.rs, ./libraries/rust/client/src/fixed_term/util.rs, third party dependencies and economic attacks.
Files and Repository
(a) Repository: glow-v1
(c) Items in scope:
  • ./programs/margin-pool/Cargo.toml
  • ./programs/margin-pool/Xargo.toml
  • ./programs/margin-pool/src/instructions/margin_refresh_position.rs
↓ Expand ↓
Out-of-Scope: ./programs/test-service/Cargo.toml, ./programs/test-service/Xargo.toml, ./programs/test-service/src/instructions/slippy/mod.rs, ./programs/test-service/src/instructions/mod.rs, ./programs/test-service/src/instructions/if_not_initialized.rs, ./programs/test-service/src/instructions/tokens/token_update_pyth_price.rs, ./programs/test-service/src/instructions/tokens/token_register.rs, ./programs/test-service/src/instructions/tokens/token_create.rs, ./programs/test-service/src/instructions/tokens/mod.rs, ./programs/test-service/src/instructions/tokens/token_init_native.rs, ./programs/test-service/src/instructions/tokens/token_request.rs, ./programs/test-service/src/util.rs, ./programs/test-service/src/error.rs, ./programs/test-service/src/lib.rs, ./programs/test-service/src/state/tokens.rs, ./programs/test-service/src/state/mod.rs, ./programs/test-service/src/state/slippy.rs, ./programs/turbo.json, ./programs/package.json, ./libraries/rust/tools/Cargo.toml, ./libraries/rust/tools/src/lookup_tables.rs, ./libraries/rust/tools/src/lib.rs, ./libraries/rust/solana-client/Cargo.toml, ./libraries/rust/solana-client/src/lookup_tables.rs, ./libraries/rust/solana-client/src/transaction.rs, ./libraries/rust/solana-client/src/util/pubkey.rs, ./libraries/rust/solana-client/src/util/mod.rs, ./libraries/rust/solana-client/src/util/data.rs, ./libraries/rust/solana-client/src/util/keypair.rs, ./libraries/rust/solana-client/src/lib.rs, ./libraries/rust/solana-client/src/signature.rs, ./libraries/rust/solana-client/src/rpc/native.rs, ./libraries/rust/solana-client/src/network.rs, ./libraries/rust/solana-client/src/rpc.rs, ./libraries/rust/client-web/Cargo.toml, ./libraries/rust/client-web/src/margin.rs, ./libraries/rust/client-web/src/solana_web3.rs, ./libraries/rust/client-web/src/error.rs, ./libraries/rust/client-web/src/lib.rs, ./libraries/rust/client-web/src/margin_pool.rs, ./libraries/rust/client-web/src/wallet.rs, ./libraries/rust/simulation/Cargo.toml, ./libraries/rust/simulation/src/runtime.rs, ./libraries/rust/simulation/src/lib.rs, ./libraries/rust/simulation/src/solana_rpc_api.rs, ./libraries/rust/testing/Cargo.toml, ./libraries/rust/testing/src/lib.rs, ./libraries/rust/environment/Cargo.toml, ./libraries/rust/environment/src/lookup_tables.rs, ./libraries/rust/environment/src/config.rs, ./libraries/rust/environment/src/lib.rs, ./libraries/rust/environment/src/builder/margin.rs, ./libraries/rust/environment/src/builder/global.rs, ./libraries/rust/environment/src/builder/margin_pool.rs, ./libraries/rust/environment/src/client_config.rs, ./libraries/rust/environment/src/builder.rs, ./libraries/rust/margin/Cargo.toml, ./libraries/rust/margin/src/margin_account_ext.rs, ./libraries/rust/margin/src/ix_builder.rs, ./libraries/rust/margin/src/lookup_tables.rs, ./libraries/rust/margin/src/tokens.rs, ./libraries/rust/margin/src/util/queue_processor.rs, ./libraries/rust/margin/src/util/asynchronous.rs, ./libraries/rust/margin/src/util/no_dupe_queue.rs, ./libraries/rust/margin/src/util/mod.rs, ./libraries/rust/margin/src/refresh/position_refresher.rs, ./libraries/rust/margin/src/refresh/deposit.rs, ./libraries/rust/margin/src/refresh/mod.rs, ./libraries/rust/margin/src/refresh/pool.rs, ./libraries/rust/margin/src/lib.rs, ./libraries/rust/margin/src/test_service.rs, ./libraries/rust/margin/src/solana/transaction.rs, ./libraries/rust/margin/src/solana/mod.rs, ./libraries/rust/margin/src/tx_builder/airspace.rs, ./libraries/rust/margin/src/tx_builder/invoke_pool.rs, ./libraries/rust/margin/src/tx_builder/user.rs, ./libraries/rust/margin/src/tx_builder/invoke_context.rs, ./libraries/rust/margin/src/tx_builder/mod.rs, ./libraries/rust/margin/src/get_state/rpc_query.rs, ./libraries/rust/margin/src/get_state/margin_state.rs, ./libraries/rust/margin/src/get_state/mod.rs, ./libraries/rust/margin/src/margin_integrator.rs, ./libraries/rust/static-program-registry/Cargo.toml, ./libraries/rust/static-program-registry/src/lib.rs, ./libraries/rust/client/Cargo.toml, ./libraries/rust/client/src/margin.rs, ./libraries/rust/client/src/client.rs, ./libraries/rust/client/src/config.rs, ./libraries/rust/client/src/lib.rs, ./libraries/rust/client/src/test_service.rs, ./libraries/rust/client/src/margin_pool.rs, ./libraries/rust/client/src/state/lookup_tables.rs, ./libraries/rust/client/src/state/margin.rs, ./libraries/rust/client/src/state/tokens.rs, ./libraries/rust/client/src/state/oracles.rs, ./libraries/rust/client/src/state/margin_pool.rs, ./libraries/rust/client/src/wallet.rs, ./libraries/rust/client/src/state.rs, ./libraries/rust/client/src/fixed_term/util.rs, third party dependencies and economic attacks.
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

1

Low

7

Informational

8

Security analysisRisk levelRemediation Date
Registry account can be closed by an arbitrary userMediumSolved - 02/18/2025
Missing mint extensions validationLowPartially Solved - 03/13/2025
Realloc re-initialization wastes compute unitsLowSolved - 02/20/2025
Two step authority transfer not implementedLowSolved - 02/24/2025
Unverified program parameters leading to misconfiguration risksLowSolved - 03/09/2025
Liquidators may leave liquidated accounts unhealthyLowSolved - 03/15/2025
Administrator can transfer positions to another userLowAcknowledged - 03/04/2025
Airspace authorities can manipulate arbitrary metadata accountsLowSolved - 03/09/2025
Permit revocation of a revoked regulator cannot be done by anyone as intendedInformationalSolved - 01/22/2025
Fee owner cannot withraw feesInformationalSolved - 03/02/2025
Risk of undefined behavior due to stack overflowInformationalSolved - 02/19/2025
Unused TokenConfig fields result in incorrect token standard determinationInformationalSolved - 12/19/2024
PermitIssuerRevoke helper function uses incorrect instruction dataInformationalSolved - 02/27/2025
Unnecessary unsafe codeInformationalSolved - 02/27/2025
Risk of incorrect interest rate calculationInformationalSolved - 02/27/2025
Risk of unhandled panicsInformationalSolved - 03/03/2025

7. Findings & Tech Details

7.1 Registry account can be closed by an arbitrary user

//

Medium

Description

The close_registry_account instruction allows users to close a lookup table registry account, provided the account contains no lookup table entries.


However, this instruction does not verify whether the caller is authorized to close the account, meaning anyone can invoke it. As a result, an unauthorized user could close someone else’s account and claim the account's rent.


Additionally, the Lookup Table Registry program does not permit users to reduce the size of a RegistryAccount when removing entries. Instead, the rent increases with each reallocation as the account grows. In extreme cases, the rent for a fully allocated account (with a maximum size of 10,272 bytes) could reach approximately 0.07 SOL.


lookup-table-registry/src/lib.rs

pub struct CloseRegistryAccount<'info> {
    #[account(mut,
        close = recipient,
        constraint = registry_account.len == 0 @ ErrorCode::RegistryNotEmpty
    )]
    pub registry_account: Box<Account<'info, RegistryAccount>>,

    /// The authority of the registry account
    pub authority: Signer<'info>,

    /// The recipient of lamports
    #[account(mut)]
    pub recipient: Signer<'info>,
}
Proof of Concept
  1. Initialize registry account with authority A.

  2. Close registry account with different signing user.

BVSS
Recommendation

To address this issue, it is recommended to allow only authorized users (the creator of the registry and eventually the program administrator) to close the registry accounts.

Remediation Comment

SOLVED: The Glow team resolved this finding by requiring the RegistryAccount authority to sign the instruction.

7.2 Missing mint extensions validation

//

Low

Description

The Margin Pool Adapter program allows an authority to create token pools using any Token2022 token and to transfer tokens into and out of the pool vault. However, the program does not validate whether the pool vault’s mint has any associated extensions, which may introduce security risks.


For example:

  • If the mint includes the TransferFeeConfig extension, a fee is automatically deducted from each transfer. This means the amount received in the destination pool vault token account will be less than expected, potentially causing data inconsistencies in deposits and loan repayments. The recorded deposit/repayment amount may not match the actual amount transferred.

  • If the mint includes the PermanentDelegate extension, a designated delegate has unrestricted permissions to transfer and burn tokens from any account associated with that mint, which could lead to unauthorized fund movements.


create_pool.rs

/// The mint for the token being custodied by the pool
pub token_mint: Box<InterfaceAccount<'info, Mint>>,
BVSS
Recommendation

To address this issue, it is recommended to validate and restrict the use of mints with potentially harmful extensions.

Remediation Comment

PARTIALLY SOLVED: The Glow team partially resolved this issue by validating underlying mints during pool creation and rejecting those with potentially harmful extensions. However, to support the PYUSD (PayPal USD) token, which has the PermanentDelegate extension enabled, this extension needed to remain allowed. To mitigate potential risks, it is necessary to manually review all newly created pools and their underlying mints, ensuring that only mints with the PermanentDelegate extension from trusted third parties are accepted.

7.3 Realloc re-initialization wastes compute units

//

Low

Description

The set_entry instruction of the Metadata program and the create_lookup_table instruction of the Lookup Table Registry program both use the realloc function to expand the size of data accounts used by these programs and re-initialize the new memory to zero.


The realloc function includes a boolean parameter that determines whether the memory should be re-initialized to zero. Re-initializing memory is helpful for preventing the reuse of old data when the program first decreases the memory size and later increases it again. However, when the account size is only increased, re-initialization is unnecessary because the runtime already provides pre-allocated, zero-filled memory. Performing this re-initialization in such cases results in wasted compute units.

As both instructions can only increase the account data size, memory re-initialization is therefore not necessary.


metadata/src/lib.rs

pub fn set_entry(ctx: Context<SetEntry>, offset: u64, data: Vec<u8>) -> Result<()> {
    // Check if the metadata account needs to be resized
    let metadata_account = ctx.accounts.metadata_account.to_account_info();

    let offset: usize = offset as usize;
    let data_len = data.len() + offset;
    let account_len = metadata_account.data_len();
    if account_len < data_len {
        // We need to realloc
        let rent = Rent::get()?;
        let transfer_amount = rent
            .minimum_balance(data_len)
            .saturating_sub(metadata_account.lamports());

        if transfer_amount > 0 {
            anchor_lang::system_program::transfer(
                CpiContext::new(
                    ctx.accounts.system_program.to_account_info(),
                    anchor_lang::system_program::Transfer {
                        from: ctx.accounts.payer.to_account_info(),
                        to: metadata_account.clone(),
                    },
                ),
                transfer_amount,
            )?;
        }

        metadata_account.realloc(data_len, true)?;
    }

lookup-table-registry/src/lib.rs

if append_to_end {
    // Happy case, add to the end
    let registry_info = ctx.accounts.registry_account.to_account_info();
    let existing_len = registry_info.data_len();
    registry_info.realloc(existing_len + REGISTRY_ENTRY_SIZE, true)?;
    ctx.accounts.registry_account.tables.push(entry);
} else {
BVSS
Recommendation

To address this issue it is recommended to disable the memory re-initialization feature of the realloc function when the account data size is only being increased (without first being decreased and then increased again).

Remediation Comment

SOLVED: The Glow team solved this finding by disabling the memory re-initialization.

7.4 Two step authority transfer not implemented

//

Low

Description

The instructions set_governor and airspace_set_authority allow an authority to set a new authority of the protocol and the airspace, respectively.


However, both instructions pass the new authority is as Public key via instruction parameters and do not require the new authority to sign the instruction. Accidentally setting an incorrect authority would cause loss of control over the protocol or airspace.


set_governor.rs

pub fn set_governor_handler(ctx: Context<SetGovernor>, new_governor: Pubkey) -> Result<()> {
    ctx.accounts.governor_id.governor = new_governor;

    Ok(())
}

airspace_set_authority.rs

pub fn airspace_set_authority_handler(
    ctx: Context<AirspaceSetAuthority>,
    new_authority: Pubkey,
) -> Result<()> {
    let airspace = &mut ctx.accounts.airspace;

    airspace.authority = new_authority;

    emit!(AirspaceAuthoritySet {
        airspace: airspace.key(),
        authority: new_authority
    });

    Ok(())
}
BVSS
Recommendation

To address this issue, it is recommended to require the new authority to sign the instruction as well, ensuring they possess the private key.

Remediation Comment

SOLVED: The Glow team resolved this finding by implementing a two-step process. First, an instruction is executed to propose a new authority. Then, a second instruction requires the proposed authority to finalize the transfer by providing its signature.

7.5 Unverified program parameters leading to misconfiguration risks

//

Low

Description

The configure_token instruction allows the airspace authority to set and update token configurations. However, the parameters value_modifier, max_staleness, admin and token_kind are not validated, leaving them open to being set to arbitrary values:


  • max_staleness: If this parameter is incorrectly set to 0 or an excessively large value, it will effectively disable the program's staleness verification mechanism.

  • value_modifier: If this parameter is set to a value greater than 100, it could enable the issuance of under-collateralized loans.

  • token_kind: Accidentally updating the token kind from Claim to Collateral or vice versa would corrupt the data integrity of the protocol that might cause losses for the protocol or for the users.

  • admin: Accidentally setting an incorrect account as token administrator could compromise the token price results. In addition the instruction refresh_deposit_position does not verify the correct owner of the Pyth oracle price account which could be an issue in case the oracle is set incorrectly during token configuration.


configure_token.rs

pub fn configure_token_handler(
    ctx: Context<ConfigureToken>,
    updated_config: Option<TokenConfigUpdate>,
) -> Result<()> {
    let config = &mut ctx.accounts.token_config;

    emit!(TokenConfigured {
        airspace: ctx.accounts.airspace.key(),
        mint: ctx.accounts.mint.key(),
        update: updated_config.clone(),
    });

    let updated_config = match updated_config {
        Some(update) => update,
        None => return config.close(ctx.accounts.payer.to_account_info()),
    };

    if config.underlying_mint != Pubkey::default()
        && updated_config.underlying_mint != config.underlying_mint
    {
        msg!("underlying mint cannot be changed");
        return err!(ErrorCode::InvalidConfig);
    }

    config.mint = ctx.accounts.mint.key();
    config.airspace = ctx.accounts.airspace.key();
    config.underlying_mint = updated_config.underlying_mint;
    config.admin = updated_config.admin;
    config.token_kind = updated_config.token_kind;
    config.value_modifier = updated_config.value_modifier;
    config.max_staleness = updated_config.max_staleness;

    config.validate()?;

    Ok(())
}

The configure margin pool instruction allows the airspace authority to set and update pool configurations. However non of the MarginPoolConfig parameters are validated, leaving them open to being set to arbitrary values.


The program does not guarantee, that the parameters utilization_rate_* and borrow_rate_* are set with increasing values which could cause unexpected behavior. The parameter management_fee_rate is not capped to 10000 and setting it to greater value could result in incorrect fee and interest calculations.


configure.rs

pub fn configure_handler(
    ctx: Context<Configure>,
    metadata: Option<TokenMetadataParams>,
    config: Option<MarginPoolConfig>,
    oracle: Option<TokenPriceOracle>,
) -> Result<()> {
    let pool = &mut ctx.accounts.margin_pool;

    if let Some(new_config) = config {
        pool.config = new_config;
    }

Proper validation of these parameters is essential to ensure the program functions as intended and avoids potential misuse.

BVSS
Recommendation

To address this issue, it is recommended to validate the value_modifier and max_staleness parameters by restricting their values to predefined acceptable ranges. The token_kind parameter must be set only during the initial token configuration and cannot not be allowed to be modified afterward. When setting the admin parameter, the program must verify that the oracle price account has correct data format and is owned by the Pyth program.

Validate the MarginPoolConfig parameters of the configure instruction and make sure their values are in expected ranges.

Remediation Comment

SOLVED: The Glow team solved this finding by adding validation checks to the parameters of the configure_token and configure instructions, ensuring they can only be updated with expected and meaningful values. Additionally, the oracle price account implementation was refactored, making the validation of the price account in the admin parameter unnecessary.

7.6 Liquidators may leave liquidated accounts unhealthy

//

Low

Description

The instructions liquidate_begin, liquidator_invoke and liquidate_end are used by the authorized liquidators to initiate, execute and end liquidations of unhealthy accounts. However the liquidation process does not guarantee that a liquidated account will be healthy after the liquidation ends. A liquidator can liquidate an account only partially, take a liquidation fee and leave the account unhealthy. This process may be repeated until the liquidated account will be drained.


liquidate_end.rs

pub fn liquidate_end_handler(ctx: Context<LiquidateEnd>) -> Result<()> {
    let mut account = ctx.accounts.margin_account.load_mut()?;
    let start_time = ctx.accounts.liquidation.load()?.state.start_time();

    let timed_out = Clock::get()?.unix_timestamp - start_time >= LIQUIDATION_TIMEOUT;

    if (account.liquidator != ctx.accounts.authority.key()) && !timed_out {
        msg!(
            "Only the liquidator may end the liquidation before the timeout of {} seconds",
            LIQUIDATION_TIMEOUT
        );
        return Err(ErrorCode::UnauthorizedLiquidator.into());
    }

    account.end_liquidation();

    emit!(events::LiquidationEnded {
        margin_account: ctx.accounts.margin_account.key(),
        authority: ctx.accounts.authority.key(),
        timed_out,
    });

    Ok(())
}

margin/src/state/account.rs

pub fn end_liquidation(&mut self) {
    self.liquidator = Pubkey::default();
}
BVSS
Recommendation

To address this issue, it is recommended to incentivize liquidators to fully resolve unhealthy accounts by allowing them to collect liquidation fees only after the account has been restored to a healthy state.

Remediation Comment

SOLVED: The Glow team resolved this issue by introducing a mechanism that accumulates the liquidator's fees during the liquidation process. Liquidators can only collect their fees once the account has been restored to a healthy state.

7.7 Administrator can transfer positions to another user

//

Low

Description

The admin_transfer_position instruction enables the protocol administrator to transfer any position from one MarginAccount to another.


According to the documentation, this functionality is intended as a mechanism for manually resolving issues in the protocol caused by problematic user assets.


However, the instruction does not verify that both MarginAccounts belong to the same owner. As a result, the administrator could potentially transfer positions between accounts owned by different users, which could lead to the loss of user funds.


admin_transfer_position.rs

pub struct AdminTransferPosition<'info> {
    /// The administrative authority
    #[account(address = GOVERNOR_ID)]
    pub authority: Signer<'info>,

    /// The target margin account to move a position into
    #[account(mut)]
    pub target_account: AccountLoader<'info, MarginAccount>,

    /// The source account to move a position out of
    #[account(mut)]
    pub source_account: AccountLoader<'info, MarginAccount>,

    /// The token account to be moved from
    #[account(mut, token::mint = token_mint, token::token_program = token_program)]
    pub source_token_account: InterfaceAccount<'info, TokenAccount>,

    /// The token account to be moved into
    #[account(mut, token::mint = token_mint)]
    pub target_token_account: InterfaceAccount<'info, TokenAccount>,

    pub token_mint: InterfaceAccount<'info, Mint>,

    pub token_program: Interface<'info, TokenInterface>,
}
BVSS
Recommendation

To address this issue, it is recommended to ensure that both the source and target MarginAccounts belong to the same owner. This safeguard will prevent accidental transfers between accounts owned by different users.

Remediation Comment

ACKNOWLEDGED: The Glow team acknowledged this finding but chose to maintain the existing implementation. The instruction is intended for scenarios where an account cannot be liquidated or in rare, extreme circumstances.

7.8 Airspace authorities can manipulate arbitrary metadata accounts

//

Low

Description

The set_entry and remove_entry instructions in the Metadata program allow airspace authorities to create or delete metadata accounts. However, these instructions do not verify that the provided metadata account has the correct PDA (Program Derived Address). As a result, any airspace authority can modify or remove any metadata account, even if it is not associated with the corresponding airspace.


This issue could lead to metadata manipulation or data inconsistencies within the system.


metadata/src/lib.rs

#[derive(Accounts)]
#[instruction(len: u16)]
pub struct SetEntry<'info> {
    /// The address paying the rent for the account if additional rent is required
    #[account(mut)]
    pub payer: Signer<'info>,

    /// The account containing the metadata to change
    /// CHECK:
    #[account(mut)]
    pub metadata_account: AccountInfo<'info>,

    /// The authority that must sign to make this change
    pub authority: Signer<'info>,

    /// The airspace that the entry belongs to]
    #[cfg_attr(not(feature = "testing"), account(
        constraint = airspace.authority == authority.key(),
    ))]
    pub airspace: Box<Account<'info, Airspace>>,

    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct RemoveEntry<'info> {
    /// The account containing the metadata to change
    /// CHECK: This is safe because we can only mutate accounts that we own, and we are
    /// closing this account. The metadata program only has entries, so this is then
    /// presumed to always be an entry. The risk could be if we close a different type
    /// of account unintentionally.
    #[account(mut)]
    pub metadata_account: AccountInfo<'info>,

    /// The authority that must sign to make this change
    pub authority: Signer<'info>,

    /// The airspace that the entry belongs to
    #[cfg_attr(not(feature = "testing"), account(
        constraint = airspace.authority == authority.key(),
    ))]
    pub airspace: Box<Account<'info, Airspace>>,

    /// The address receiving the rent
    /// CHECK: Some details
    #[account(mut)]
    pub receiver: AccountInfo<'info>,
}
Proof of Concept
  1. Create two margin pools: poolA and poolB with their associated airspaces (airspace1 and airspace2) and different airspace authorities (authority1 and autority2).

  2. Use airspace authority1 to remove a metadata account associated with airspace2.

BVSS
Recommendation

To address this issue it is recommended to verify the address of the metadata account and ensure that it is associated with the expected airspace.

Remediation Comment

SOLVED: The Glow team solved this finding by verifying that the provided metadata account is correctly linked to the specified airspace using the appropriate seeds for the metadata's PDA.

7.9 Permit revocation of a revoked regulator cannot be done by anyone as intended

//

Informational

Description

The instruction airspace_permit_revoke allows to revoke permits. The intended behavior based on the documentation is that "For restricted airspaces, anyone can revoke a permit from a revoked regulator.".


However the instruction incorrectly requires the signature of the airspace authority or a permit issuer and thus does not allow any user to revoke the permit of a revoked regulator.


airspace_permit_revoke.rs

// The airspace authority or issuing regulator is always allowed to revoke
if authority != airspace.authority && authority != permit.issuer {
    return err!(AirspaceErrorCode::PermissionDenied);
}
BVSS
Recommendation

To address this issue, it is recommended to re-evaluate the requirement stating that "for restricted airspaces, anyone can revoke a permit from a revoked regulator." If this requirement has a valid business justification, the implementation of the airspace_permit_revoke instruction should be adjusted accordingly. However, it is important to consider that allowing unrestricted revocation would enable anyone to close the affected permit accounts and claim the associated rent fees.

Remediation Comment

SOLVED: The Glow team resolved this finding by aligning the code to the documentation and allowing anyone to revoke a permit if an airspace is restricted and the issuer is revoked.

7.10 Fee owner cannot withraw fees

//

Informational

Description

The withdraw_fees instruction is intended to allow the fee owner to withdraw collected fees. However, it consistently fails due to an incorrect token account authority during the burning of deposit notes. Currently, the authority is set to margin_pool, but it should be set to fee_owner for the instruction to execute successfully.


withdraw_fees.rs

fn burn_note_context(&self) -> CpiContext<'_, '_, '_, 'info, Burn<'info>> {
    CpiContext::new(
        self.pool_token_program.to_account_info(),
        Burn {
            from: self.fee_destination.to_account_info(),
            mint: self.deposit_note_mint.to_account_info(),
            // authority: self.margin_pool.to_account_info(),
            authority: self.fee_owner.to_account_info(),
        },
    )
}
BVSS
Recommendation

To address this issue, it is recommended to update the deposit notes authority to fee_owner when burning tokens.

Remediation Comment

SOLVED: The Glow team resolved this finding by setting the correct authority when burning tokens.

7.11 Risk of undefined behavior due to stack overflow

//

Informational

Description

The margin pool instruction create_pool uses too many stack allocated variables which causes the following error during program compilation:

Compiling glow-margin-pool v1.0.0 (./glow-v1/programs/margin-pool)
Error: Function _ZN165_$LT$glow_margin_pool..instructions..create_pool..CreatePool$u20$as$u20$anchor_lang..Accounts$LT$glow_margin_pool..instructions..create_pool..CreatePoolBumps$GT$$GT$12try_accounts17ha2829e083fe80a7eE Stack offset of 6896 exceeded max offset of 4096 by 2800 bytes, please minimize large stack variables

Stack overflow can lead to undefined behavior and currently blocks deployment to devnet and mainnet. This issue arises due to multiple factors:

  1. Anchor 0.30.1 overuses stack variables – See this issue.

  2. Differences in feature activation – Devnet and mainnet lack certain features available on localnet, as detailed here.

BVSS
Recommendation

To address this issue, it is recommended to reduce the size of stack variables by using the Box type and allocating variables on the heap, using zero_copy or splitting instructions into multiple smaller instructions.

Remediation Comment

SOLVED: The Glow team resolved this issue by manually initializing accounts instead of using Anchor's init constraint, effectively reducing stack usage.

7.12 Unused TokenConfig fields result in incorrect token standard determination

//

Informational

Description

The configure_tokens instruction allows adding new token configurations or updating existing ones in the tokens whitelist. However, this instruction does not explicitly set the TokenConfig::mint_token_program and TokenConfig::underlying_mint_token_program fields, leaving them at their default values.


As a result, when a user registers a new position using the register_position instruction, the program incorrectly determines the associated token standard. The program compares the TokenConfig::mint_token_program or TokenConfig::underlying_mint_token_program addresses based on the type of asset and checks if they match the address of the Token 2022 program. If they do not match, the AccountPosition::is_token_2022 field is set to 0, indicating that the legacy token program is being used. Since the TokenConfig program addresses are never set, the is_token_2022 field is always set to 0, even when the Token 2022 program was actually used.


While this issue does not pose a security risk, it breaks the existing client code and helper functions that interact with the protocol.


configure_token.rs

config.mint = ctx.accounts.mint.key();
config.airspace = ctx.accounts.airspace.key();
config.underlying_mint = updated_config.underlying_mint;
config.admin = updated_config.admin;
config.token_kind = updated_config.token_kind;
config.value_modifier = updated_config.value_modifier;
config.max_staleness = updated_config.max_staleness;

positions.rs

pub fn new_from_config(
    config: &Account<TokenConfig>,
    mint_decimals: u8,
    address: Pubkey,
    adapter: Pubkey,
) -> Self {
    Self {
        mint: config.mint,
        token_program: match config.token_kind {
            TokenKind::Collateral => config.underlying_mint_token_program,
            TokenKind::Claim => config.mint_token_program,
            TokenKind::AdapterCollateral => config.mint_token_program,
        },

account.rs

if let Some(free_position) = free_position {
    free_position.exponent = -(config.decimals as i16);
    free_position.address = config.address;
    free_position.adapter = config.adapter;
    free_position.kind = config.kind.into_integer();
    free_position.balance = 0;
    free_position.value_modifier = config.value_modifier;
    free_position.max_staleness = config.max_staleness;
    // NIT: This isn't a great way of indicating token support, because what happens if
    // there is token_2026 in future?
    free_position.is_token_2022 = if config.token_program == anchor_spl::token_2022::ID {
        1
    } else {
        0
    };
BVSS
Recommendation

To address this issue, it is recommended to correctly set the TokenConfig::mint_token_program and TokenConfig::underlying_mint_token_program fields during token configuration in the token_configure instruction. This will enable correct determination of the token program during position registration.

Remediation Comment

SOLVED: The Glow team resolved this finding by correctly setting the respective TokenConfig fields.

7.13 PermitIssuerRevoke helper function uses incorrect instruction data

//

Informational

Description

The library function permit_issuer_revoke is a helper function to create the AirspacePermitIssuerRevoke instruction. However this function accidentally creates an instruction with AirspacePermitIssuerCreate data instead of AirspacePermitIssuerRevoke data.


Sending such malformed instruction will cause the instruction to fail. This issue does not have any security implications, but may cause problems for example during testing where the tests will unexpectedly fail.


airspace.rs

pub fn permit_issuer_revoke(&self, issuer: Pubkey) -> Instruction {
    let accounts = glow_airspace::accounts::AirspacePermitIssuerRevoke {
        airspace: self.address,
        authority: self.authority,
        receiver: self.payer,
        issuer_id: self.derive_issuer_id(&issuer),
    }
    .to_account_metas(None);

    Instruction {
        accounts,
        program_id: glow_airspace::ID,
        data: glow_airspace::instruction::AirspacePermitIssuerCreate { issuer }.data(),
    }
}
BVSS
Recommendation

To address this issue, it is recommended to change the AirspacePermitIssuerCreate data to AirspacePermitIssuerRevoke.

Remediation Comment

SOLVED: The Glow team resolved this finding by setting the correct instruction data.

7.14 Unnecessary unsafe code

//

Informational

Description

The instruction AirspacePermitCreate uses unsafe code to disable lifetime checker. However the use of unsafe code is not necessary in this case. Unsafe code in Solana programs is considered bad practice and should be avoided in order to prevent unexpected behavior.


airspace_permit_create.rs

// If the airspace is not restricted, then any signer can create permits
if airspace.is_restricted && airspace.authority != authority.key() {
    // For a restricted airspace, the optional regulator account needs to be verified
    // to prove that the signer is authorized to create the permit
    let _ = Account::<AirspacePermitIssuerId>::try_from(unsafe {
        // 2024-11-05: The transmute is used to extend lifetimes, as it's otherwise been impossible to solve the lifetime
        // constraints here.
        // This is expected to be resolved in a future Anchor version: https://github.com/coral-xyz/anchor/pull/3340
        std::mem::transmute::<&AccountInfo<'_>, &AccountInfo<'_>>(&ctx.accounts.issuer_id)
    })?;

    // No further checks are necessary, since the address is already verified by anchor,
    // and the account data being valid to deserialize means the permission was granted
}
BVSS
Recommendation

To address this issue, it is recommended to replace the unsafe code by a safe alternative such as: let = AirspacePermitIssuerId::try_deserialize( &mut &ctx.accounts.issuer_id.data.try_borrow().unwrap()[..], );


This code is safe and will verify that the issuer_id account was correctly initialized as expected.

Remediation Comment

SOLVED: The Glow team resolved this finding by removing the unsafe code and implementing the recommended solution.

7.15 Risk of incorrect interest rate calculation

//

Informational

Description

The MarginPool::interest_rate method calculates the current interest rate for loans. It also handles the edge case where the pool is empty and contains zero deposit notes.


Currently, when the pool is empty, the method returns borrow_rate_1, which represents the transition point between the first and second interest rate regimes. However, the correct rate in this scenario should be borrow_rate_0, as it corresponds to the starting point of the first regime.


Although this does not currently cause incorrect results—since the interest rate is multiplied by zero when the pool is empty—it should still be corrected to prevent potential issues in future code updates.


margin-pool/src/state.rs

pub fn interest_rate(&self) -> Number {
    let borrow_1 = Number::from_bps(self.config.borrow_rate_1);

    // Catch the edge case of empty pool
    if self.deposit_notes == 0 {
        return borrow_1;
    }
BVSS
Recommendation

To address this issue it is recommended to return the borrow_rate_0 interest rate when the pool is empty.

Remediation Comment

SOLVED: The Glow team resolved this issue by returning the borrow_rate_0 interest rate when the pool is empty.

7.16 Risk of unhandled panics

//

Informational

Description

The liquidate_begin instruction allows a liquidator to initiate a liquidation. As part of its logic, the instruction checks whether the account is already undergoing liquidation.


If the same liquidator attempts to invoke this instruction twice, the program will panic by calling the unreachable!() macro. This macro is typically intended for cases where the compiler cannot infer that a specific branch of code should never be reached.


However, in this scenario, it is entirely possible for the same liquidator to call liquidate_begin twice. Therefore, using the unreachable!() macro is inappropriate.


liquidate_begin.rs

// verify not already being liquidated
match account.liquidator {
    liq if liq == liquidator => {
        // this liquidator has already been set as the active liquidator,
        // so there is nothing to do
        unreachable!();
    }

    liq if liq == Pubkey::default() => {
        // not being liquidated, so claim it
        account.start_liquidation(liquidator);
    }

    _ => {
        // already claimed by some other liquidator
        return Err(ErrorCode::Liquidating.into());
    }
}
BVSS
Recommendation

To resolve this issue, it is recommended to handle cases where the same liquidator invokes the liquidate_begin instruction twice by either treating it as a no-op or returning a meaningful error message.

Remediation Comment

SOLVED: The Glow team resolved this finding by ensuring that multiple invocations of the liquidate_begin instruction by the same liquidator have no effect.

8. Automated Testing

Static Analysis Report
Description

Halborn used automated security scanners to assist with detection of well-known security issues and vulnerabilities. Among the tools used was cargo audit, a security scanner for vulnerabilities reported to the RustSec Advisory Database. All vulnerabilities published in https://crates.io are stored in a repository named The RustSec Advisory Database. cargo audit is a human-readable version of the advisory database which performs a scanning on Cargo.lock. Security Detections are only in scope. All vulnerabilities shown here were already disclosed in the above report. However, to better assist the developers maintaining this code, the auditors are including the output with the dependencies tree, and this is included in the cargo audit output to better know the dependencies affected by unmaintained and vulnerable crates.


Cargo Audit Results

ID

Crate

Description

RUSTSEC-2024-0344

curve25519-dalek

Timing variability in curve25519-dalek's Scalar29::sub/Scalar52::sub

RUSTSEC-2022-0093

ed25519-dalek

Double Public Key Signing Function Oracle Attack on ed25519-dalek

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.

© Halborn 2025. All rights reserved.