Halborn Logo

Endogenous AVS - Solayer


Prepared by:

Halborn Logo

HALBORN

Last Updated 08/13/2024

Date of Engagement by: July 30th, 2024 - August 2nd, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

9

Critical

0

High

0

Medium

0

Low

3

Informational

6


1. Introduction

Solayer team engaged Halborn to conduct a security assessment on their Endogenous AVS Solana program beginning on July 30th, 2024, and ending on August, 5th, 2024. The security assessment was scoped to the Solana Program provided in endoavs-program GitHub repository. Commit hashes and further details can be found in the Scope section of this report.

The Endogenous AVS program takes the sSOL liquid mint and transforms it into a synthetic asset representing the delegation to a particular project, using the delegate instruction. These mints can be undelegated instantly if there is a need for trade, through the undelegate instruction.

Partners will be able to create an endoavs account through the create instruction, passing a mint address which they can customize. The authority can customize the AVS token name, symbol, uri/url and metadata of these assets through instructions. The authority can also transfer the authority to other account, which is irrevocable.

These assets use the same liquidity as the underlying sSOL. Ultimately, the goal is to enable Solayer to provide stake-weighted quality of service to the AVS.

2. Assessment Summary

Halborn was provided 6 days for the engagement and assigned one full-time security engineer to review the security of the Solana Program 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 Endogenous AVS Solana Program.

    • Ensure that the program's functionality operates as intended.

In summary, Halborn identified some low-severity and informational security issues, that were addressed and acknowledged by the Solayer team. The main ones were the following:

    • System Flooding and Spamming.

    • Lack of two-step authority transfer.

    • Decimals should be enforced.

    • Missing URI and URL prefix validation.

    • Missing Metadata size validation.

    • Missing Event emissions.

    • Outdated dependencies.


Overall, the program in-scope is adherent to Solana's best-practices and carries consistent code quality.

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 (anchor test).

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:
EXPLOITABILIY 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: restaking-program
(b) Assessed Commit ID: 547e66a
(c) Items in scope:
  • src/contexts/delegate.rs
  • src/contexts/metadata.rs
  • src/contexts/create.rs
↓ Expand ↓
Out-of-Scope:
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

3

Informational

6

Security analysisRisk levelRemediation Date
System Flooding and SpammingLowSolved - 08/12/2024
Missing URI and URL prefix validationLowRisk Accepted
Missing Metadata size validationLowRisk Accepted
Lack of two-step Authority transferInformationalAcknowledged
Missing Event emissionsInformationalAcknowledged
Lack of Zero Amount validationInformationalAcknowledged
Un-sanitized on-chain state can be used as attack vectorInformationalAcknowledged
Use of 'msg!' consumes additional computational budgetInformationalAcknowledged
Outdated dependenciesInformationalSolved - 08/12/2024

7. Findings & Tech Details

7.1 System Flooding and Spamming

// Low

Description

The current implementation of the endo_avs account creation process allows for the creation of multiple accounts with the same AVS name and does not enforce a minimum delegate amount upon AVS's creation. This combination can be exploited by malicious actors to flood the system with endo_avs accounts that use the same name/symbol, for misleading and griefing purposes, effectively spamming the system with unvalid accounts.

The endo_avs metadata is then initialized with default values for name, symbol and uri. These parameters can be changed by the current endo_avs authority through the process described further.

- programs/endoavs-program/src/contexts/create.rs

#[derive(Accounts)]
pub struct CreateEndoAVS<'info> {
    #[account(
        init,
        payer = authority,
        seeds = [b"endo_avs", avs_token_mint.key().as_ref()],
        bump,
        space = 8 + EndoAVS::INIT_SPACE
    )]
    pub endo_avs: Account<'info, EndoAVS>,
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(
        init,
        payer = authority,
        mint::decimals = delegated_token_mint.decimals,
        mint::authority = endo_avs,
        mint::freeze_authority = endo_avs
    )]
    pub avs_token_mint: Box<InterfaceAccount<'info, Mint>>,
    #[account(
        mut,
        address=Metadata::find_pda(&avs_token_mint.key()).0
    )]
    pub avs_token_metadata: UncheckedAccount<'info>,
    #[account(
        init_if_needed,
        payer = authority,
        associated_token::mint = delegated_token_mint,
        associated_token::authority = endo_avs,
        associated_token::token_program = token_program
    )]
    pub delegated_token_vault: Box<InterfaceAccount<'info, TokenAccount>>,
    #[account(
        mint::token_program = token_program,
        constraint = allow_as_delegated_asset(&delegated_token_mint.key()) @ EndoAVSError::UnsupportedAsset
    )]
    pub delegated_token_mint: Box<InterfaceAccount<'info, Mint>>,
    pub token_program: Interface<'info, TokenInterface>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_metadata_program: Program<'info, Metaplex>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

impl<'info> CreateEndoAVS<'info> {
    pub fn create(&mut self, bumps: CreateEndoAVSBumps, name: String) -> Result<()> {
        if name.len() > MAX_ENDO_AVS_NAME_LENGTH {
            return Err(EndoAVSError::NameTooLong.into());
        }

        self.endo_avs.set_inner(EndoAVS {
            name,
            url: "".to_string(),
            bump: bumps.endo_avs,
            authority: self.authority.key(),
            avs_token_mint: self.avs_token_mint.key(),
            delegated_token_mint: self.delegated_token_mint.key(),
            delegated_token_vault: self.delegated_token_vault.key(),
        });

        let token_metadata = DataV2 {
            name: DEFAULT_ENDO_AVS_NAME.to_string(),
            symbol: DEFAULT_ENDO_AVS_SYMBOL.to_string(),
            uri: DEFAULT_ENDO_AVS_URI.to_string(),
            seller_fee_basis_points: 0,
            creators: None,
            collection: None,
            uses: None,
        };

        let bump = [self.endo_avs.bump];
        let signer_seeds: [&[&[u8]]; 1] = [&[
            b"endo_avs",
            self.avs_token_mint.to_account_info().key.as_ref(),
            &bump,
        ][..]];

        let metadata_ctx = CpiContext::new_with_signer(
            self.token_metadata_program.to_account_info(),
            CreateMetadataAccountsV3 {
                payer: self.authority.to_account_info(),
                update_authority: self.endo_avs.to_account_info(),
                mint: self.avs_token_mint.to_account_info(),
                metadata: self.avs_token_metadata.to_account_info(),
                mint_authority: self.endo_avs.to_account_info(),
                system_program: self.system_program.to_account_info(),
                rent: self.rent.to_account_info(),
            },
            &signer_seeds,
        );

        create_metadata_accounts_v3(metadata_ctx, token_metadata, true, true, None)?;

        Ok(())
    }
}

After properly initializing an endo_avs account, it is possible to change its name, symbol, uri and url through the update_token_metadata and update_endoavs instructions.

- programs/endoavs-program/src/contexts/manage.rs

#[derive(Accounts)]
pub struct UpdateEndoAVSInfo<'info>{
    pub authority: Signer<'info>,
    #[account(
        mut,
        has_one = authority, // permission check
        has_one = avs_token_mint,
        seeds = [b"endo_avs", avs_token_mint.key().as_ref()],
        bump = endo_avs.bump,
    )]
    pub endo_avs: Account<'info, EndoAVS>,
    pub avs_token_mint: Box<InterfaceAccount<'info, Mint>>,
    pub system_program: Program<'info, System>,
}

impl <'info> UpdateEndoAVSInfo<'info> {
    pub fn update(&mut self, name: Option<String>, url: Option<String>) -> Result<()> {
        if let Some(name) = name {
            require!(name.len() < MAX_ENDO_AVS_NAME_LENGTH, EndoAVSError::NameTooLong);
            self.endo_avs.name = name;
        }
        if let Some(url) = url {
            require!(url.len() < MAX_ENDO_AVS_URL_LENGTH, EndoAVSError::URLTooLong);
            self.endo_avs.url = url;
        }
        Ok(())
    }
}

- programs/endoavs-program/src/contexts/metadata.rs

#[derive(Accounts)]
pub struct AVSTokenMetadata<'info> {
    #[account(
        seeds = [b"endo_avs", avs_token_mint.key().as_ref()],
        bump = endo_avs.bump,
        has_one = avs_token_mint,
        has_one = authority,
    )]
    pub endo_avs: Account<'info, EndoAVS>,
    #[account(mut)]
    pub authority: Signer<'info>,
    pub avs_token_mint: Box<InterfaceAccount<'info, Mint>>,
    #[account(
        mut,
        address=Metadata::find_pda(&avs_token_mint.key()).0
    )]
    pub avs_token_metadata: UncheckedAccount<'info>,
    pub token_metadata_program: Program<'info, Metaplex>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

impl<'info> AVSTokenMetadata<'info> {
    pub fn update(&mut self, name: String, symbol: String, uri: String) -> Result<()> {
        if !symbol.ends_with(REQUIRED_TOKEN_SYMBOL_SUFFIX) {
            return Err(EndoAVSError::InvalidTokenSymbol.into());
        }

        let token_metadata = DataV2 {
            name,
            symbol,
            uri,
            seller_fee_basis_points: 0,
            creators: None,
            collection: None,
            uses: None,
        };

        let bump = [self.endo_avs.bump];
        let signer_seeds: [&[&[u8]]; 1] = [&[
            b"endo_avs",
            self.avs_token_mint.to_account_info().key.as_ref(),
            &bump,
        ][..]];

        let metadata_ctx = CpiContext::new_with_signer(
            self.token_metadata_program.to_account_info(),
            anchor_spl::metadata::UpdateMetadataAccountsV2 {
                metadata: self.avs_token_metadata.to_account_info(),
                update_authority: self.endo_avs.to_account_info(),
            },
            &signer_seeds,
        );

        anchor_spl::metadata::update_metadata_accounts_v2(
            metadata_ctx,
            None,
            token_metadata.into(),
            None,
            None,
        )?;

        Ok(())
    }
}

During the whole cycle, there are no mechanisms in place to prevent malicious users from creating a significant high amount of dummy or fake Endo AVS accounts.This vulnerability has several negative consequences:

  1. System Flooding: Malicious actors can create a large number of invalid accounts, overwhelming the system and potentially causing operational disruptions.

  2. User Confusion: The presence of multiple spam accounts with the same symbol but invalid tokens can confuse users, leading them to interact with illegitimate accounts, which can ultimately rug legitimate users through abusing the mint authority. This confusion can result in permanent financial loss for users and undermine trust in the platform.

  3. Platform Legitimacy: The proliferation of spam or invalid accounts can erode the legitimacy of the platform, as users may perceive it as unreliable or insecure.

Proof of Concept

In order to reproduce this vulnerability, the following test case can be used. It will use the same signer to create multiple endo_avs accounts with the same name/symbol, without delegating any amount to these newly created accounts.

PoC Code:

    it.only("should fail to create multiple EndoAVS with same name/symbol", async () => {
        /// using User B in this test.
        try {
            await program.methods.create("mksSOL")
            .accounts({
                endoAvs: endo_avs_attacker,
                authority: d34db33f_account.publicKey,
                avsTokenMint: avs_token_mint_attacker.publicKey,
                avsTokenMetadata: metaplex.nfts().pdas().metadata({ mint: avs_token_mint_attacker.publicKey }),
                delegatedTokenVault: getAssociatedTokenAddressSync(delegate_token_mint, endo_avs_attacker, true),
                delegatedTokenMint: delegate_token_mint,
                tokenProgram: TOKEN_PROGRAM_ID,
                associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                tokenMetadataProgram: metadata_program,
                systemProgram: SystemProgram.programId,
                rent: anchor.web3.SYSVAR_RENT_PUBKEY
            }).signers([d34db33f_account, avs_token_mint_attacker]).rpc();
            const endoavs_info = await program.account.endoAvs.fetch(endo_avs_attacker);
            assert.ok(endoavs_info);
            console.log("endoavs_info is : ", JSON.stringify(endoavs_info, null, 2));

            await program.methods.create("mksSOL")
            .accounts({
                endoAvs: endo_avs_attacker2,
                authority: d34db33f_account.publicKey,
                avsTokenMint: avs_token_mint_attacker2.publicKey,
                avsTokenMetadata: metaplex.nfts().pdas().metadata({ mint: avs_token_mint_attacker2.publicKey }),
                delegatedTokenVault: getAssociatedTokenAddressSync(delegate_token_mint, endo_avs_attacker2, true),
                delegatedTokenMint: delegate_token_mint,
                tokenProgram: TOKEN_PROGRAM_ID,
                associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                tokenMetadataProgram: metadata_program,
                systemProgram: SystemProgram.programId,
                rent: anchor.web3.SYSVAR_RENT_PUBKEY
            }).signers([d34db33f_account, avs_token_mint_attacker2]).rpc();
            const endoavs_info2 = await program.account.endoAvs.fetch(endo_avs_attacker2);
            assert.ok(endoavs_info2);
            console.log("endoavs_info2 is : ", JSON.stringify(endoavs_info2, null, 2));

            await program.methods.create("mksSOL")
            .accounts({
                endoAvs: endo_avs_attacker3,
                authority: d34db33f_account.publicKey,
                avsTokenMint: avs_token_mint_attacker3.publicKey,
                avsTokenMetadata: metaplex.nfts().pdas().metadata({ mint: avs_token_mint_attacker3.publicKey }),
                delegatedTokenVault: getAssociatedTokenAddressSync(delegate_token_mint, endo_avs_attacker3, true),
                delegatedTokenMint: delegate_token_mint,
                tokenProgram: TOKEN_PROGRAM_ID,
                associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                tokenMetadataProgram: metadata_program,
                systemProgram: SystemProgram.programId,
                rent: anchor.web3.SYSVAR_RENT_PUBKEY
            }).signers([d34db33f_account, avs_token_mint_attacker3]).rpc();
            const endoavs_info3 = await program.account.endoAvs.fetch(endo_avs_attacker3);
            assert.ok(endoavs_info3);
            console.log("endoavs_info3 is : ", JSON.stringify(endoavs_info3, null, 2));

        } catch (error) {
            assert(error.message);
            console.error(error);
        }
    });

Stack traces:

  endoavs-program::
endoavs_info is :  {
  "bump": 255,
  "authority": "FYTjAm73BmkAFDm9VqBAtsryAVkjUPge9EG7HSNjcqeq",
  "avsTokenMint": "EzByfLuvkTaRdKSoafLGEZX2Pw2e433dx1Kuat3FcEsT",
  "delegatedTokenMint": "sSo14endRuUbvQaJS3dq36Q829a3A6BEfoeeRGJywEh",
  "delegatedTokenVault": "CSiRqvu3nvzH4NQHidWt11wgwz8WMUS1A5JSMEGWD6Lq",
  "name": "mksSOL",
  "url": ""
}
endoavs_info2 is :  {
  "bump": 253,
  "authority": "FYTjAm73BmkAFDm9VqBAtsryAVkjUPge9EG7HSNjcqeq",
  "avsTokenMint": "5pqkYdEh5xpsiaRBNxs6LTnsswGrw4U7NzxMRhqnGFKW",
  "delegatedTokenMint": "sSo14endRuUbvQaJS3dq36Q829a3A6BEfoeeRGJywEh",
  "delegatedTokenVault": "135jLTgYrmMivNF2Eqx5qCYBp7eSvGRkYsmiM4tTFzeX",
  "name": "mksSOL",
  "url": ""
}
endoavs_info3 is :  {
  "bump": 255,
  "authority": "FYTjAm73BmkAFDm9VqBAtsryAVkjUPge9EG7HSNjcqeq",
  "avsTokenMint": "25LkCus9gAquyDMnpupDoqWgUqosCUiQM9s6rU3coKGC",
  "delegatedTokenMint": "sSo14endRuUbvQaJS3dq36Q829a3A6BEfoeeRGJywEh",
  "delegatedTokenVault": "ED25CKR64L9gWo9Uvt71V64ZHuBEFsETUv4BMq4XzPtT",
  "name": "mksSOL",
  "url": ""
}

Execution:

poc_01

BVSS
Recommendation
  1. Enforce Unique Symbols:

Create a PDA symbol_mapping to track existing symbols, with a boolean exists field. This PDA will use the symbol as a seed to ensure uniqueness.

In the create instruction, add a symbol_mapping account (Program Derived Address). Use the init account constraint and the symbol as a seed for the PDA derivation. This will effectively block duplicate symbols from being used to create new endo_avs accounts, preventing system flooding. Additionally, the create function should set the exists field of the provided PDA to true.

This approach ensures that each endo_avs account has a unique symbol, mitigating the risk of system flooding and maintaining the integrity of the platform.


2. Enforce a Minimum delegation amount upon endo_avs creation:

Implement a minimum delegation amount requirement upon the creation of endo_avs accounts. This will discourage malicious users from creating numerous low-value accounts, as they will have no financial incentive and will incur a direct loss of sSOL.

By enforcing a minimum delegation amount, you can deter malicious actors from flooding the system with invalid accounts, thereby enhancing the security and reliability of the platform.


Remediation Plan

SOLVED: The Solayer team has solved this issue by enforcing a minimum deposit fee. The commit hash containing the modifications is d379d7898a98d4403f8305896bc3faf7e162cf44.

Remediation Hash

7.2 Missing URI and URL prefix validation

// Low

Description

The authority of each endo_avs account is entitled to change the name and the url through the update method, as follows:

- programs/endoavs-program/src/contexts/manage.rs

impl <'info> UpdateEndoAVSInfo<'info> {
    pub fn update(&mut self, name: Option<String>, url: Option<String>) -> Result<()> {
        if let Some(name) = name {
            require!(name.len() < MAX_ENDO_AVS_NAME_LENGTH, EndoAVSError::NameTooLong);
            self.endo_avs.name = name;
        }
        if let Some(url) = url {
            require!(url.len() < MAX_ENDO_AVS_URL_LENGTH, EndoAVSError::URLTooLong);
            self.endo_avs.url = url;
        }
        Ok(())
    }

When updating the metadata, this verification is also missing.

- programs/endoavs-program/src/contexts/metadata.rs

impl<'info> AVSTokenMetadata<'info> {
    pub fn update(&mut self, name: String, symbol: String, uri: String) -> Result<()> {
        if !symbol.ends_with(REQUIRED_TOKEN_SYMBOL_SUFFIX) {
            return Err(EndoAVSError::InvalidTokenSymbol.into());
        }

        let token_metadata = DataV2 {
            name,
            symbol,
            uri,
            seller_fee_basis_points: 0,
            creators: None,
            collection: None,
            uses: None,
        };

        let bump = [self.endo_avs.bump];
        let signer_seeds: [&[&[u8]]; 1] = [&[
            b"endo_avs",
            self.avs_token_mint.to_account_info().key.as_ref(),
            &bump,
        ][..]];

        let metadata_ctx = CpiContext::new_with_signer(
            self.token_metadata_program.to_account_info(),
            anchor_spl::metadata::UpdateMetadataAccountsV2 {
                metadata: self.avs_token_metadata.to_account_info(),
                update_authority: self.endo_avs.to_account_info(),
            },
            &signer_seeds,
        );

        anchor_spl::metadata::update_metadata_accounts_v2(
            metadata_ctx,
            None,
            token_metadata.into(),
            None,
            None,
        )?;

        Ok(())
    }
}

There are no verifications in place whether the provided url or uri starts with an expected format, such as https://, what could lead to unintended behavior in off-chain premises and also pollute the account with inaccurate information.

BVSS
Recommendation

It is recommended to add simple verifications to check whether the provided uri and url prefixes matches pre-determined formats.


Remediation Plan

RISK ACCEPTED: The Solayer team has accepted the risk related to this finding.

7.3 Missing Metadata size validation

// Low

Description

The authority account of each endo_avs is entitled to update its metadata information, including name, symbol and uri, as strings. However, there are no verifications in place to prevent those user-provided inputs from being excessively large.

- programs/endoavs-program/src/contexts/metadata.rs

impl<'info> AVSTokenMetadata<'info> {
    pub fn update(&mut self, name: String, symbol: String, uri: String) -> Result<()> {
        if !symbol.ends_with(REQUIRED_TOKEN_SYMBOL_SUFFIX) {
            return Err(EndoAVSError::InvalidTokenSymbol.into());
        }

        let token_metadata = DataV2 {
            name,
            symbol,
            uri,
            seller_fee_basis_points: 0,
            creators: None,
            collection: None,
            uses: None,
        };

        let bump = [self.endo_avs.bump];
        let signer_seeds: [&[&[u8]]; 1] = [&[
            b"endo_avs",
            self.avs_token_mint.to_account_info().key.as_ref(),
            &bump,
        ][..]];

        let metadata_ctx = CpiContext::new_with_signer(
            self.token_metadata_program.to_account_info(),
            anchor_spl::metadata::UpdateMetadataAccountsV2 {
                metadata: self.avs_token_metadata.to_account_info(),
                update_authority: self.endo_avs.to_account_info(),
            },
            &signer_seeds,
        );

        anchor_spl::metadata::update_metadata_accounts_v2(
            metadata_ctx,
            None,
            token_metadata.into(),
            None,
            None,
        )?;

        Ok(())
    }
}

The lack of validation of user-provided inputs for excessively large values can lead to unintended behavior in off-chain premises, such as weird website rendering, and also pollute the system state with inadequate data.

BVSS
Recommendation

It is recommended to check the length of user-provided inputs against a safe threshold.


Remediation Plan

RISK ACCEPTED: The Solayer team has accepted the risk related to this finding.

7.4 Lack of two-step Authority transfer

// Informational

Description

The endoavs program in-scope allows the current authority account of each Endogenous AVS to transfer the authority to another account. Such action is permanent and irrevocable.

The existing implementation of the transfer_authority instruction employs a one-step procedure for authority delegation, which presents a security concern. This method lacks a safeguard against inadvertent delegations or potential hostile takeovers.

- programs/endoavs-program/src/contexts/manage.rs

impl<'info> TransferAuthority<'info> {
    pub fn transfer_authority(&mut self) -> Result<()> {
        self.endo_avs.authority = self.new_authority.key();
        msg!("Transferred authority to {}", self.new_authority.key());
        Ok(())
    }
}
BVSS
Recommendation

To resolve this issue, it is advisable to establish a two-step process for authority transfer, thereby enhancing the security of the operation. The current authority would first propose a new candidate authority, who would then need to formally accept the role.

This process would be structured as follows:

  1. Proposal by Current Authority: The current signer proposes a new candidate signer. This action updates the candidate_authority field in the endo_avs account's state. This step assumes the prior creation of an additional field candidate_authority on state/endoavs.rs.

  2. Acceptance by New Authority: The proposed candidate_authority formally accepts the role. This step transfers the authority status from the current authority to the candidate_authority.


Remediation Plan

ACKNOWLEDGED: The Solayer team has acknowledged this finding.

7.5 Missing Event emissions

// Informational

Description

It is considered best practice when developing Solana programs to emit events when important modifications to the state are performed, such as Metadata modifications and authority transfers.

- programs/endoavs-program/src/contexts/manage.rs

impl<'info> TransferAuthority<'info> {
    pub fn transfer_authority(&mut self) -> Result<()> {
        self.endo_avs.authority = self.new_authority.key();
        msg!("Transferred authority to {}", self.new_authority.key());
        Ok(())
    }
}

- programs/endoavs-program/src/contexts/manage.rs

impl <'info> UpdateEndoAVSInfo<'info> {
    pub fn update(&mut self, name: Option<String>, url: Option<String>) -> Result<()> {
        if let Some(name) = name {
            require!(name.len() < MAX_ENDO_AVS_NAME_LENGTH, EndoAVSError::NameTooLong);
            self.endo_avs.name = name;
        }
        if let Some(url) = url {
            require!(url.len() < MAX_ENDO_AVS_URL_LENGTH, EndoAVSError::URLTooLong);
            self.endo_avs.url = url;
        }
        Ok(())
    }
}

- programs/endoavs-program/src/contexts/metadata.rs

impl<'info> AVSTokenMetadata<'info> {
    pub fn update(&mut self, name: String, symbol: String, uri: String) -> Result<()> {
        if !symbol.ends_with(REQUIRED_TOKEN_SYMBOL_SUFFIX) {
            return Err(EndoAVSError::InvalidTokenSymbol.into());
        }

        let token_metadata = DataV2 {
            name,
            symbol,
            uri,
            seller_fee_basis_points: 0,
            creators: None,
            collection: None,
            uses: None,
        };

        let bump = [self.endo_avs.bump];
        let signer_seeds: [&[&[u8]]; 1] = [&[
            b"endo_avs",
            self.avs_token_mint.to_account_info().key.as_ref(),
            &bump,
        ][..]];

        let metadata_ctx = CpiContext::new_with_signer(
            self.token_metadata_program.to_account_info(),
            anchor_spl::metadata::UpdateMetadataAccountsV2 {
                metadata: self.avs_token_metadata.to_account_info(),
                update_authority: self.endo_avs.to_account_info(),
            },
            &signer_seeds,
        );

        anchor_spl::metadata::update_metadata_accounts_v2(
            metadata_ctx,
            None,
            token_metadata.into(),
            None,
            None,
        )?;

        Ok(())
    }
}

It was identified that events are not being emitted for the annotated important state operations.

BVSS
Recommendation

Ensure that all critical actions within the program emit corresponding events, such as when transferring the authority or updating token information.


Remediation Plan

ACKNOWLEDGED: The Solayer team has acknowledged this finding.

7.6 Lack of Zero Amount validation

// Informational

Description

The program in-scope does not prevent the delegate and undelegate methods from being called with amount == 0.

- programs/endoavs-program/src/contexts/delegate.rs

    pub fn delegate(&mut self, amount: u64) -> Result<()> {
        // Transfer tokens from user to the delegated token vault
        let transfer_accounts = TransferChecked {
            from: self.staker_delegated_token_account.to_account_info(),
            to: self.delegated_token_vault.to_account_info(),
            mint: self.delegated_token_mint.to_account_info(),
            authority: self.staker.to_account_info(),
        };
        let transfer_ctx = CpiContext::new(self.token_program.to_account_info(), transfer_accounts);
        transfer_checked(transfer_ctx, amount, self.delegated_token_mint.decimals)?;

        let bump = [self.endo_avs.bump];
        let signer_seeds: [&[&[u8]]; 1] = [&[
            b"endo_avs",
            self.avs_token_mint.to_account_info().key.as_ref(),
            &bump,
        ][..]];

        let mint_ctx = CpiContext::new_with_signer(
            self.token_program.to_account_info(),
            MintTo {
                to: self.staker_avs_token_account.to_account_info(),
                mint: self.avs_token_mint.to_account_info(),
                authority: self.endo_avs.to_account_info(),
            },
            &signer_seeds[..],
        );
        mint_to(mint_ctx, amount)?;

        Ok(())
    }

- programs/endoavs-program/src/contexts/delegate.rs

    pub fn undelegate(&mut self, amount: u64) -> Result<()> {
        // Burn EndoAVS tokens from the user
        let burn_accounts = Burn {
            from: self.staker_avs_token_account.to_account_info(),
            mint: self.avs_token_mint.to_account_info(),
            authority: self.staker.to_account_info(),
        };
        let burn_ctx = CpiContext::new(self.token_program.to_account_info(), burn_accounts);
        burn(burn_ctx, amount)?;

        let bump = [self.endo_avs.bump];
        let signer_seeds: [&[&[u8]]; 1] = [&[
            b"endo_avs",
            self.avs_token_mint.to_account_info().key.as_ref(),
            &bump,
        ][..]];

        let transfer_accounts = TransferChecked {
            from: self.delegated_token_vault.to_account_info(),
            to: self.staker_delegated_token_account.to_account_info(),
            mint: self.delegated_token_mint.to_account_info(),
            authority: self.endo_avs.to_account_info(),
        };
        let transfer_ctx = CpiContext::new_with_signer(
            self.token_program.to_account_info(),
            transfer_accounts,
            &signer_seeds,
        );
        transfer_checked(transfer_ctx, amount, self.delegated_token_mint.decimals)?;

        Ok(())
    }

While this condition does not lead to immediate financial loss, it should be checked to keep overall consistency.

Score
Recommendation

Consider adding a verification before the execution of the delegate and undelegate methods, blocking those actions with amount == 0.


Remediation Plan

ACKNOWLEDGED: The Solayer team has acknowledged this finding.

7.7 Un-sanitized on-chain state can be used as attack vector

// Informational

Description

The current implementation of methods that change endo_avs token informations lacks proper input validation, which can lead to security vulnerabilities.

Metadata, such as name, symbol, and uri, and also token name and symbol, can be freely provided by users when creating or updating EndoAVS metadata. If not properly sanitized in the front-end, this metadata can be used as an attack vector for Stored Cross-Site Scripting (XSS), and other well-known web vulnerabilities.

- programs/endoavs-program/src/contexts/manage.rs

impl <'info> UpdateEndoAVSInfo<'info> {
    pub fn update(&mut self, name: Option<String>, url: Option<String>) -> Result<()> {
        if let Some(name) = name {
            require!(name.len() < MAX_ENDO_AVS_NAME_LENGTH, EndoAVSError::NameTooLong);
            self.endo_avs.name = name;
        }
        if let Some(url) = url {
            require!(url.len() < MAX_ENDO_AVS_URL_LENGTH, EndoAVSError::URLTooLong);
            self.endo_avs.url = url;
        }
        Ok(())
    }

impl<'info> AVSTokenMetadata<'info> {
    pub fn update(&mut self, name: String, symbol: String, uri: String) -> Result<()> {
        if !symbol.ends_with(REQUIRED_TOKEN_SYMBOL_SUFFIX) {
            return Err(EndoAVSError::InvalidTokenSymbol.into());
        }

        let token_metadata = DataV2 {
            name,
            symbol,
            uri,
            seller_fee_basis_points: 0,
            creators: None,
            collection: None,
            uses: None,
        };

        let bump = [self.endo_avs.bump];
        let signer_seeds: [&[&[u8]]; 1] = [&[
            b"endo_avs",
            self.avs_token_mint.to_account_info().key.as_ref(),
            &bump,
        ][..]];

        let metadata_ctx = CpiContext::new_with_signer(
            self.token_metadata_program.to_account_info(),
            anchor_spl::metadata::UpdateMetadataAccountsV2 {
                metadata: self.avs_token_metadata.to_account_info(),
                update_authority: self.endo_avs.to_account_info(),
            },
            &signer_seeds,
        );

        anchor_spl::metadata::update_metadata_accounts_v2(
            metadata_ctx,
            None,
            token_metadata.into(),
            None,
            None,
        )?;

        Ok(())
    }
}

If an attacker manages to successfully craft valid payloads using on-chain state to weaponize it, the attack can ultimately lead to critical consequences such as account take-over and arbitrary script execution on victim's browser.

Score
Recommendation

Given that on-chain primitives do not allow for proper sanitization of inputs, and considering the myriad of payloads that can affect web applications, it is recommended to conduct a thorough examination of off-chain components, such as back-end and front-end applications.

Ensure that malicious on-chain payloads are properly sanitized in off-chain premises to prevent payloads from being rendered in a way that could execute arbitrary code in users' browsers.


Remediation Plan

ACKNOWLEDGED: The Solayer team has acknowledged this finding.

7.8 Use of 'msg!' consumes additional computational budget

// Informational

Description

The usage of msg! is usually advisable during tests, and will incur in additional computational budget when the instruction is processed in Mainnet.

- programs/endoavs-program/src/contexts/manage.rs

impl<'info> TransferAuthority<'info> {
    pub fn transfer_authority(&mut self) -> Result<()> {
        self.endo_avs.authority = self.new_authority.key();
        msg!("Transferred authority to {}", self.new_authority.key());
        Ok(())
    }
}
Score
Recommendation

Consider removing debugging messages before Mainnet deployment.


Remediation Plan

ACKNOWLEDGED: The Solayer team has acknowledged this finding.

7.9 Outdated dependencies

// Informational

Description

It was identifying during the assessment of the program endo_avs in-scope that its dependencies for the Anchor framework and also for Solana are not current.

[[package]]
name = "solana-program"
version = "1.18.7"
[[package]]
name = "anchor-lang"
version = "0.29.0"

Score
Recommendation

It is recommended to update dependencies to their current versions, as specified:

  • Solana: v1.18.20

  • Anchor: v0.31.0


Remediation Plan

SOLVED: The Solayer team has solved this issue as recommended. The commit hash containing the modification is 46c09073a6dad390f435dc76f17e35849f2c6d1b.

Remediation Hash

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

Desccription

RUSTSEC-2022-0093

ed25519-dalek

Double Public Key Signing Function Oracle Attack on ed255109-dalek

RUSTSEC-2024-0344

curve25519-dalek

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

RUSTSEC-2021-0145

atty

Potential unaligned read

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 2024. All rights reserved.