Prepared by:
HALBORN
Last Updated 10/14/2024
Date of Engagement by: August 26th, 2024 - September 11th, 2024
100% of all REPORTED Findings have been addressed
All findings
7
Critical
0
High
0
Medium
0
Low
3
Informational
4
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.
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
.
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
)
EXPLOITABILIY METRIC () | METRIC VALUE | NUMERICAL 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 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL 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 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
0
Medium
0
Low
3
Informational
4
Security analysis | Risk level | Remediation Date |
---|---|---|
Deadline depends on block number instead of timestamp | Low | Risk Accepted - 09/19/2024 |
Lack of Revert in Mira's get_y Function Poses Convergence Risk | Low | Risk Accepted - 09/19/2024 |
Strict Deadline Check Prevents Exact Block Height Matching | Low | Solved - 09/19/2024 |
Single-Step Ownership Transfer in AMM Contract | Informational | Acknowledged - 09/19/2024 |
Prevalence of Magic Numbers in Codebase | Informational | Acknowledged - 09/19/2024 |
Potential infinite loop | Informational | Acknowledged - 09/19/2024 |
Redundant call of validate_pool_id in burn | Informational | Solved - 09/23/2024 |
// Low
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.
The implementation of the check_deadline
function is in:
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.
As done in the Timelock
example in the sway-examples
repo:
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.
RISK ACCEPTED: The Mira team accepted the risk of this finding.
// Low
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:
Inaccurate price calculations.
Unfair trade executions.
Potential manipulation of pool balances.
Loss of funds for liquidity providers or traders.
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
}
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.
// Low
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.
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");
}
SOLVED: Non-strict check has been implemented by the Mira team.
// Informational
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.
It is recommended to implement a two-step ownership transfer process:
The current owner proposes a new owner.
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.
ACKNOWLEDGED: As the Fuel standard still does not support 2-step ownership transfer, the Mira team decided to keep it as is for now.
// Informational
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
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
ACKNOWLEDGED: The Mira team acknowledged the finding but will keep it as is to reproduce the same code as UniV2 and Velodrome V1.
// Informational
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.
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");
}
ACKNOWLEDGED: The Mira team decided to keep it as is because it is the user's responsibility to prove adequate input parameters.
// Informational
The burn
function contains a redundant call to validate_pool_id
:
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
:
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()
}
Remove the first call to validate_pool_id
.
SOLVED: The Mira team solved this issue.
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.
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.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed