Halborn Logo

Mira v1 Core & Periphery - Mira


Prepared by:

Halborn Logo

HALBORN

Last Updated 10/14/2024

Date of Engagement by: August 26th, 2024 - September 11th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

7

Critical

0

High

0

Medium

0

Low

3

Informational

4


1. Introduction

Mira engaged Halborn to conduct a security assessment on their smart contracts revisions beginning on 08/26/2024 and ending on 09/11/2024. The security assessment was scoped to the smart contracts provided to the Halborn team.

2. Assessment Summary

The team at Halborn was provided 3 weeks for the engagement and assigned a full-time security engineer to evaluate the security of the smart contract.

The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.

The purpose of this assessment is to:

    • Ensure that smart contract functions operate as intended.

    • Identify potential security issues with the smart contracts.


In summary, Halborn identified some minor security risks which should be addressed by the Mira team.

3. Test Approach and Methodology

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy regarding the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance code coverage and quickly identify items that do not follow the security best practices. The following phases and associated tools were used during the assessment:

    • Research into architecture and purpose.

    • Smart contract manual code review and walkthrough.

    • Graphing out functionality and contract logic/connectivity/functions.

    • Manual assessment of use and safety for the critical sway variables and functions in scope to identify any arithmetic related vulnerability classes.

    • Manual testing by custom scripts.

    • Scanning of sway files for vulnerabilities, security hot-spots or bugs.

    • Static Analysis of security for scoped contract, and imported functions. (Sway-Analyzer)

    • Testnet deployment. (Forc ToolChain)

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: mira-v1-periphery
(b) Assessed Commit ID: 14ac97e
(c) Items in scope:
  • mira_amm.sw
  • data_structures.sw
  • pool_math.sw
↓ Expand ↓
Out-of-Scope:
Files and Repository
(a) Repository: mira-v1-core
(b) Assessed Commit ID: 0ebb288
(c) Items in scope:
  • lib.sw
  • callee.sw
  • errors.sw
↓ 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

4

Security analysisRisk levelRemediation Date
Deadline depends on block number instead of timestampLowRisk Accepted - 09/19/2024
Lack of Revert in Mira's get_y Function Poses Convergence RiskLowRisk Accepted - 09/19/2024
Strict Deadline Check Prevents Exact Block Height MatchingLowSolved - 09/19/2024
Single-Step Ownership Transfer in AMM ContractInformationalAcknowledged - 09/19/2024
Prevalence of Magic Numbers in CodebaseInformationalAcknowledged - 09/19/2024
Potential infinite loopInformationalAcknowledged - 09/19/2024
Redundant call of validate_pool_id in burnInformationalSolved - 09/23/2024

7. Findings & Tech Details

7.1 Deadline depends on block number instead of timestamp

// Low

Description

The market uses block height instead of timestamps for transaction deadlines. This is flawed as they are not a reliable source of time as not all blocks are created after the same time, leading to transactions being executed at a later time than expected.

Proof of Concept

The implementation of the check_deadline function is in:

https://github.com/mira-amm/mira-v1-periphery/blob/349cfa420120f0a7379af9babcc29b3fb6945946/libraries/utils/src/blockchain_utils.sw#L7C1-L10C2

pub fn check_deadline(deadline: u32) {
    require(deadline >= height(), "Deadline passed");
}

The function compares the provided deadline to the current block height, rather than the current timestamp, which is flawed as explained in the previous section.

BVSS
Recommendation

As done in the Timelock example in the sway-examples repo:

https://github.com/FuelLabs/sway-applications/blob/master/timelock/timelock-contract/src/main.sw#L131C1-L137C11

        let start = timestamp() + MINIMUM_DELAY;
        let end = timestamp() + MAXIMUM_DELAY;

        require(
            start <= time && time <= end,
            TransactionError::TimestampNotInRange((start, end, time)),
        );

compare the deadline with the current timestamp instead.

Remediation

RISK ACCEPTED: The Mira team accepted the risk of this finding.

References

7.2 Lack of Revert in Mira's get_y Function Poses Convergence Risk

// Low

Description

In Mira's implementation of the get_y function, the method does not revert if it fails to converge within 255 iterations:

fn get_y(x_0: u256, xy: u256, y: u256) -> u256 {
// ... [iteration logic] ...
    while i < 255 {
// ... [convergence attempts] ...
        i += 1;
    }
    y// Returns potentially non-converged value
}

In contrast, Velodrome's implementation reverts if convergence is not achieved:

function _get_y(uint256 x0, uint256 xy, uint256 y) internal view returns (uint256) {
    for (uint256 i = 0; i < 255; i++) {
// ... [convergence attempts] ...
    }
    revert("!y");// Reverts if no convergence
}

The lack of reversion in Mira's implementation introduces a vulnerability. If the function fails to converge, it returns a potentially incorrect y value.

In the case of high volatility or extreme scenario, this can lead to:

  1. Inaccurate price calculations.

  2. Unfair trade executions.

  3. Potential manipulation of pool balances.

  4. Loss of funds for liquidity providers or traders.

BVSS
Recommendation

It is recommended to modify Mira's get_y function to revert if convergence is not achieved.

fn get_y(
    x_0: u256,
    xy: u256,
    y: u256
) -> u256 {
    let mut y: u256 = y;

    let mut i = 0;
    while i < 255 {
        // ... // 
    }
    
    // If we reach here, convergence was not achieved
    assert(false, "Y not found");
    0 // This line is never reached due to the assert, but Sway requires a return value
}
Remediation

RISK ACCEPTED: The Mira team wants to keep the Velodrome V1 format and will not implement a change, moreover risk is handled on core part.

References

7.3 Strict Deadline Check Prevents Exact Block Height Matching

// Low

Description

The check_deadline function in the blockchain_utils library implements a strict inequality check for the deadline:

pub fn check_deadline(deadline: u32) {
    require(deadline > height(), "Deadline passed");
}

This implementation only allows transactions to be executed when the current block height is strictly less than the specified deadline. It does not allow transactions to be executed at the exact block height specified as the deadline.

This strict inequality check results in a loss of precision in deadline enforcement. Users intending to set a deadline for the exact block height N will find their transactions failing at block height N, even though they intended to allow execution up to and including that block. This behavior is inconsistent with common expectations and can lead to unnecessary transaction failures, particularly in time-sensitive operations or when precise control over execution timing is required.


BVSS
Recommendation

It is recommended to modify the check_deadline function to use a greater than or equal to comparison:

pub fn check_deadline(deadline: u32) {
    require(deadline >= height(), "Deadline passed");
}

Remediation

SOLVED: Non-strict check has been implemented by the Mira team.

Remediation Hash
References

7.4 Single-Step Ownership Transfer in AMM Contract

// Informational

Description

The AMM contract implements a single-step ownership transfer function that allows immediate transfer of contract ownership without verification:

#[storage(read, write)]
fn transfer_ownership(new_owner: Identity) {
    if _owner() == State::Uninitialized {
        initialize_ownership(new_owner);
    } else {
        transfer_ownership(new_owner);
    }
}

This implementation lacks a crucial two-step verification process. The current owner can transfer ownership directly to a new address without requiring the new owner to accept the transfer.

These vulnerabilities can lead to complete loss of contract control, potential fund lockup, and inability to perform critical administrative functions.

Score
Impact:
Likelihood:
Recommendation

It is recommended to implement a two-step ownership transfer process:

  1. The current owner proposes a new owner.

  2. The proposed owner must accept the ownership.

Example implementation:

pub struct Contract {
    owner: Identity,
    pending_owner: Option<Identity>,
}

impl Contract {
    #[storage(read, write)]
    fn transfer_ownership(new_owner: Identity) {
        require(msg.sender() == self.owner, "Not current owner");
        self.pending_owner = Some(new_owner);
    }

    #[storage(read, write)]
    fn accept_ownership() {
        require(Some(msg.sender()) == self.pending_owner, "Not pending owner");
        self.owner = msg.sender();
        self.pending_owner = None;
    }
}

This two-step process ensures that ownership is only transferred to an address that can actively accept it, significantly reducing the risk of accidental or malicious ownership loss.

Remediation

ACKNOWLEDGED: As the Fuel standard still does not support 2-step ownership transfer, the Mira team decided to keep it as is for now.

References

7.5 Prevalence of Magic Numbers in Codebase

// Informational

Description

The smart contract codebase exhibits frequent use of magic numbers, which are hardcoded numerical values without clear context or explanation. These occurrences are found across multiple files and functions. For example:

amounts_in.get(amounts_in.len() - i - 2)
0x3u256 * x_0
for i in range(255)
volatile_fee <= LP_FEE_VOLATILE / 5

These magic numbers lack clear semantic meaning and make the code less maintainable and more prone to errors during future modifications

Score
Impact:
Likelihood:
Recommendation

Replace all magic numbers with named constants that clearly describe their purpose and meaning. This practice enhances code readability, maintainability, and reduces the likelihood of errors during future updates. For example:

const MAX_ITERATIONS: u256 = 255; for i in range(MAX_ITERATIONS)
const PROTOCOL_FEE_DIVISOR: u256 = 5; volatile_fee <= LP_FEE_VOLATILE / PROTOCOL_FEE_DIVISOR
Remediation

ACKNOWLEDGED: The Mira team acknowledged the finding but will keep it as is to reproduce the same code as UniV2 and Velodrome V1.

References

7.6 Potential infinite loop

// Informational

Description

The get_amounts_out and get_amounts_in functions in the libraries/math/src/pool_math.sw file contain while loops that iterate over the entire length of the pools array. This design allows for an arbitrarily large number of iterations, potentially leading to excessive gas consumption or transaction failures if the pools array is sufficiently large.

Specific instances:

while (i < pools.len()) {
    // ... (loop body)
    i += 1;
}


2. In the get_amounts_in function:

while (i < pools.len()) {
    // ... (loop body)
    i += 1;
}

In both cases, the number of loop iterations is directly tied to the size of the pools array. If this array becomes excessively large, it could lead to significant gas costs or even cause transactions to fail by exceeding the block gas limit.

Score
Recommendation

To prevent potential infinite loops, it is recommended to add an explicit break condition based on a maximum number of iterations:

const MAX_ITERATIONS: u64 = 256;
let mut iteration_count: u64 = 0;
while (i < pools.len() && iteration_count < MAX_ITERATIONS) {
    // ... (loop body)
    i += 1;
    iteration_count += 1;
}
if iteration_count == MAX_ITERATIONS {
    revert("Maximum iteration count reached");
}

Remediation

ACKNOWLEDGED: The Mira team decided to keep it as is because it is the user's responsibility to prove adequate input parameters.

References

7.7 Redundant call of validate_pool_id in burn

// Informational

Description

The burn function contains a redundant call to validate_pool_id:

https://github.com/mira-amm/mira-v1-core/blob/e0afbeb56ba8c01e223f2552979ee55e461d24a9/contracts/mira_amm_contract/src/main.sw#L513

fn burn(pool_id: PoolId, to: Identity) -> (u64, u64) {
    reentrancy_guard();
    validate_pool_id(pool_id);

    ...
}

as it is already done when calling get_pool:

https://github.com/mira-amm/mira-v1-core/blob/e0afbeb56ba8c01e223f2552979ee55e461d24a9/contracts/mira_amm_contract/src/main.sw#L518

    fn burn(pool_id: PoolId, to: Identity) -> (u64, u64) {
        reentrancy_guard();
        validate_pool_id(pool_id);

        ...

        let mut pool = get_pool(pool_id);

        ...
fn get_pool(pool_id: PoolId) -> PoolInfo {
    let pool = get_pool_option(pool_id);
    require(pool.is_some(), InputError::PoolDoesNotExist(pool_id));
    pool.unwrap()
}
fn get_pool_option(pool_id: PoolId) -> Option<PoolInfo> {
    validate_pool_id(pool_id);
    storage.pools.get(pool_id).try_read()
}
Score
Recommendation

Remove the first call to validate_pool_id.

Remediation

SOLVED: The Mira team solved this issue.

Remediation Hash
6d4d5efcf29ce00d931bd70d14df57e867f39331
References

8. Automated Testing

Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contracts in scope. Among the tools used was Sway-Analyzer, a sway static analysis framework.

After Halborn verified the smart contracts in the repository and was able to compile them correctly into their abis and binary format, Sway-Analyze was run against the contracts. The tool makes use of the existing sway-ast and sway-parse crates in order to parse Sway source code into its abstract syntax tree (AST). A recursive AST visitor is implemented on top of this, which will walk the AST structures top-down in a context-sensitive manner.

pool-math-core.pngmira-amm.pngpool_math-amm.png


All issues identified by sway-analyzer were proved to be false positives or have been added to the issue list in this report.

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.