Halborn Logo

ES3D - Mesh Connect


Prepared by:

Halborn Logo

HALBORN

Last Updated 05/27/2024

Date of Engagement by: April 1st, 2024 - April 8th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

21

Critical

0

High

0

Medium

1

Low

5

Informational

15


1. Introduction

Mesh Connect engaged Halborn to conduct a security assessment on their smart contracts beginning on 2024-04-01 and ending on 2024-04-08. The security assessment was scoped to the smart contracts provided in the https://github.com/FrontFin/smart-contracts GitHub repository. Commit hashes and further details can be found in the Scope section of this report. The contracts in scope allow for users to transfer native assets and ERC20 tokens, and are managed by a defined Operator role.

In a follow-up engagement requested by the Mesh Connect team, Halborn reviewed the updates introduced to the protocol up to commit 88b3982. This security assessment began on 2024-05-20 and ended on 2024-05-20.

2. Assessment Summary

Halborn was provided 1 week for the first engagement, and 1 day for the second engagement, and assigned 1 full-time security engineer to review the security of the smart contracts in scope. The engineer is a blockchain and smart contract security expert with advanced penetration testing and smart contract hacking skills, and deep knowledge of multiple blockchain protocols.

The purpose of the assessment is to:

    • Identify potential security issues within the smart contracts.

    • Ensure that smart contract functionality operates as intended.

In summary, Halborn identified some improvements to reduce the likelihood and impact of risks that were mostly addressed by the Mesh Connect team. The main identified issues were the following:

    • Missing internal accounting implementation could lead to misappropriation of funds. (RISK ACCEPTED)

    • Lack of input validation in native assets transfers function might lead to loss of funds. (SOLVED)

    • Operator could front-run a transfer and modify the NativeTransfer data with unexpected fields. (SOLVED)

3. Test Approach and Methodology

Halborn performed a combination of manual and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of this assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the contracts' solidity code 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 architecture and purpose.

    • Smart contract manual code review and walk-through.

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

    • Manual testing with custom scripts (Foundry).

    • Static Analysis of security for scoped contracts, and imported functions.

3.1 Out-of-scope

    • External libraries and financial-related attacks.

    • New features/implementations after/within the remediation commit IDs.

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: smart-contracts
(b) Assessed Commit ID: 87d50d1
(c) Items in scope:
  • contracts/CeFiSmartTransfer.sol
  • contracts/DeFiSmartTransfer.sol
  • contracts/Operator.sol
Out-of-Scope:
Files and Repository
(a) Repository: smart-contracts
(b) Assessed Commit ID: 88b3982
(c) Items in scope:
  • contracts/CeFiSmartTransfer.sol
  • contracts/DeFiSmartTransfer.sol
  • contracts/Operator.sol
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

1

Low

5

Informational

15

Security analysisRisk levelRemediation Date
Missing internal accounting implementation could lead to misappropriation of fundsMediumRisk Accepted
High privileged role from Operators and Admin could put ERC20 tokens at riskLowNot Applicable
Lack of input validation in native assets transfers function might lead to loss of fundsLowSolved - 04/30/2024
Lack of input validation for amount and fee valuesLowSolved - 04/30/2024
Operator could front-run a transfer and modify the NativeTransfer data with unexpected fieldsLowSolved - 04/30/2024
Non-compliant tokens might be permanently locked in contractsLowSolved - 04/30/2024
Id fields for transfers are not uniqueInformationalAcknowledged
Check-effects-interaction pattern is not followedInformationalSolved - 05/06/2024
Transfer identifier could be stored with a cheaper value typeInformationalSolved - 05/06/2024
Redundant codeInformationalSolved - 05/23/2024
Unoptimized for loop declarationInformationalSolved - 05/06/2024
Public functions not called within the contract can be made external to save gasInformationalSolved - 05/06/2024
Unlocked pragma compilersInformationalSolved - 05/06/2024
Use of custom errors instead of revert strings may help reduce gas usageInformationalSolved - 05/06/2024
Events are missing the indexed attributeInformationalAcknowledged
Unused struct fields when calling executeNativeTransfer functionInformationalSolved - 05/06/2024
Explicit importsInformationalSolved - 05/06/2024
Transfer with hardcoded gas amountInformationalSolved - 05/06/2024
PUSH0 is not supported by all chainsInformationalAcknowledged
Lack of input validation for self-transfersInformationalAcknowledged
Variable declaration consistencyInformationalSolved - 05/23/2024

7. Findings & Tech Details

7.1 Missing internal accounting implementation could lead to misappropriation of funds

// Medium

Description

The documentation provided for the audit states the following:

Due to the sensitive native of transferring Funds, we want to make sure we track the current state of any Deposit. And in the case of an unexpected failure, we want to ensure that no Funds are left stuck in transit; instead they should be returned to the original sender.

Despite this affirmation, there is no current functionality in the CeFiSmartTransfer contract that allows for returning funds to the original sender.

Consider the following scenario:

  • Alice transfers 100 USDT tokens to the CeFiSmartTransfer contract.

  • Operator A calls executeErc20Transfer attempting to transfer the 100 tokens in the contract to Alice, but it fails or the transaction takes too long.

  • Bob transfers 500 USDC tokens to the CeFiSmartTransfer contract.

  • Carla transfers 1500 USDC tokens to the CeFiSmartTransfer contract.

  • Operator B calls executeErc20Transfer and transfers the 300 tokens in the contract to Carla.

The lack of internal accounting allows for the misallocation of funds and does not support the tracking of individual deposits, making it impossible to refund specific deposits automatically in the event of a transfer failure or delay of transactions.

Proof of Concept
  function testMisappropiationOfFunds(string memory _id, address payable _feeReceiver, address payable _receiver) public {
    vm.assume(_feeReceiver != address(0) && _feeReceiver != deployer);
    vm.assume(_receiver != address(0) && _receiver != deployer);
    vm.assume(_receiver != _feeReceiver);

    /* ---------------------------------- ALICE --------------------------------- */
    // Sends 100 tokens
    deal(address(m3shToken), alice, 100);
    vm.prank(alice);
    m3shToken.transfer(address(ceFiSmartTransfer), 100);

    /* ---------------------------------- BOB --------------------------------- */
    // Sends 500 tokens
    deal(address(m3shToken), bob, 500);
    vm.prank(bob);
    m3shToken.transfer(address(ceFiSmartTransfer), 500);

    /* ---------------------------------- CARLA --------------------------------- */
    // Sends 1500 tokens
    deal(address(m3shToken), bob, 1500);
    vm.prank(bob);
    m3shToken.transfer(address(ceFiSmartTransfer), 1500);

    assertEq(m3shToken.balanceOf(address(ceFiSmartTransfer)), 2100);

    /* -------------------------------- OPERATOR -------------------------------- */
    vm.startPrank(operator2);
    CeFi.Erc20Transfer memory _erc20Transfer = CeFi.Erc20Transfer({
      id: _id,
      amount: 2100,
      fee: 0,
      feeReceiver: payable(_feeReceiver),
      receiver: payable(_receiver),
      token: address(m3shToken)
    });
    ceFiSmartTransfer.executeErc20Transfer(_erc20Transfer);
    assertEq(m3shToken.balanceOf(address(ceFiSmartTransfer)), 0);
    assertEq(m3shToken.balanceOf(address(_receiver)), 2100);
    vm.stopPrank();
  }
BVSS
Recommendation

Consider implementing an internal accounting system for ERC20 tokens to be able to manage deposits locally and be able to return the corresponding amount to its original sender in case of delays or failures.

Remediation Plan

RISK ACCEPTED: The Mesh Connect team accepted the risk of this finding, with a thorough explanation of the approach and the rationale behind their decision not to modify the contract:

1. Off-Chain Monitoring and Handling Mechanisms:

  • We employ a comprehensive off-chain monitoring system that tracks the state of every deposit and transaction related to the CeFiSmartTransfer contract. This system is designed to detect failures or irregularities in real-time.

  • In cases of transaction failures, our off-chain mechanism is configured to initiate corrective actions, which include orchestrating the return of funds to the original sender. This approach allows for greater flexibility and responsiveness compared to handling such events solely through smart contract logic.

2. Limitations of On-Chain Error Handling for ERC20 Transfers:

  • ERC20 token transfers do not inherently trigger fallback functions in smart contracts, which limits the ability to automatically handle errors and revert transactions within the contract code itself.

  • Given this technical constraint, our solution leverages off-chain processes to manage failures effectively, ensuring that tokens are not left in a limbo state and are returned to the sender promptly.

  • Constraints with Exchange Account Transfers: In our specific operational setup, the entity initiating the transfers (the payer) is often an exchange account. This type of account typically executes direct token transfers, so we cannot utilize methods like transferFrom, which would require prior approval and subsequent action from our smart contract. This constraint further complicates the implementation of on-chain error handling, as we do not have control over the initial token transfer approval process, limiting our ability to programmatically intervene or revert transactions.

3. Security and Efficiency Considerations:

  • Managing error handling and fund returns off-chain allows us to minimize on-chain transaction costs and avoid complicating the smart contract with additional logic that could introduce new risks or vulnerabilities.

  • This approach also enhances security by reducing the attack surface within the contract and relying on controlled, auditable off-chain processes to manage critical failure scenarios.

References
CeFiSmartTransfer.sol#L1-L107

7.2 High privileged role from Operators and Admin could put ERC20 tokens at risk

// Low

Description

The DEFAULT_ADMIN_ROLE role is the only one allowed to add and remove operators for the system. The operator role-plays an integral part in managing ERC20 assets in CeFiSmartTransfer and DeFiSmartTransfer. The executeErc20Transfer function on both contracts allows operators to transfer ERC20 tokens from the contract to any address. This function is restricted to operators only, ensuring that only authorized addresses with this specified role entities can execute ERC20 transfers.

However, this design introduces a substantial risk regarding the safety and integrity of ERC20 tokens held by the CeFiSmartTransfer contract and approved to the DeFiSmartTransfer contract. Since operators have the exclusive authority to initiate transfers, the system's security heavily relies on the trustworthiness and security of these operator accounts. If an operator's account is compromised or if an operator acts maliciously, they could potentially redirect ERC20 tokens to unauthorized addresses, leading to a loss of assets.

Additionally, executeErc20Transfer functions on both contracts do not implement checks on the destination addresses or limits on the amount of tokens that can be transferred, providing operators with unrestricted access to move tokens.

BVSS
Recommendation

Several remedial strategies can be employed, including but not limited to:

Implement multi-signature control: Requiring multiple operators to approve a transfer before it can be executed adds a layer of security by distributing trust. This approach makes it much harder for a single compromised or malicious actor to execute unauthorized transfers.

Whitelist/blacklist mechanisms: Implement mechanisms to restrict token transfers to known, safe addresses, or to prevent transfers to addresses identified as risky.

Transfer limits: Introduce daily or transactional limits on the amount of ERC20 tokens that can be transferred. These limits could be configurable and subject to multi-signature approval for adjustments.

Introduce a time-lock mechanism: Implement a time-lock feature that delays the execution of token transfer requests by a certain period (e.g., 24-48 hours). This delay may allow operators and administrators of the contracts to review and potentially handle suspicious transfer requests before they are executed.

Remediation Plan

NOT APPLICABLE: According to the Mesh Connect team, this finding was addressed in their off-chain logic:

As recommended in the audit, we have implemented a time-lock mechanism to mitigate the risks associated with immediate token transfer executions by operators too. This time-lock feature in our off-chain logic, delays the execution of token transfer requests by a certain period. [...]. This changes will apply in off-chain logic.

References
CeFiSmartTransfer.sol#L95-L106
DeFiSmartTransfer.sol#L47-L65

7.3 Lack of input validation in native assets transfers function might lead to loss of funds

// Low

Description

The prepareNativeTransfer() function from the CeFiSmartTransfer contract allows for operators to set the NativeTransfer struct, which contains all the crucial information to the native asset transfer that will be executed when the contract receives assets via the receive() method. Nevertheless, this function lacks input validation for the input parameters.

Similarly, the executeNativeTransfer() function from the DeFiSmartTransfer contract allows any address to transfer native tokens to a receiver and a feeReceiver but lacks input validation for these parameters.

Such oversight on both functions can result in a potential loss of funds if either the receiver or feeReceiver are inadvertently set to address(0).


Proof of Concept
function testReceiveNativeTransferWhenFeeReceiverIsZeroAddress(
    address ethSender,
    string memory _id,
    uint256 _amount,
    uint256 _fee,
    address payable _receiver
  ) public {
    /* ------------------------------- preparation ------------------------------ */
    _notFoundryReservedAddress(_receiver);
    vm.assume(_receiver != address(0));
    vm.assume(_receiver != address(ceFiSmartTransfer));
    vm.assume(_amount != 0);
    _fee = bound(_fee, 0, _amount);

    /* -------------------------------- execution ------------------------------- */
    testPrepareNativeTransfer(_id, _amount, _fee, payable(address(0)), _receiver);
    hoax(ethSender, _amount);
    (bool ok, ) = (address(ceFiSmartTransfer)).call{value: _amount}("");
    assert(ok);
    assertEq((address(0)).balance, _fee);
}

function testReceiveNativeTransferWhenReceiverIsZeroAddress(
    address ethSender,
    string memory _id,
    uint256 _amount,
    uint256 _fee,
    address payable _feeReceiver
  ) public {
    /* ------------------------------- preparation ------------------------------ */
    _notFoundryReservedAddress(_feeReceiver);
    vm.assume(_feeReceiver != address(0));
    vm.assume(_feeReceiver != address(ceFiSmartTransfer));
    vm.assume(_amount != 0);
    _fee = bound(_fee, 0, _amount);

    /* -------------------------------- execution ------------------------------- */
    testPrepareNativeTransfer(_id, _amount, _fee, _feeReceiver, payable(address(0)));
    hoax(ethSender, _amount);
    (bool ok, ) = (address(ceFiSmartTransfer)).call{value: _amount}("");
    assert(ok);
    assertEq((address(0)).balance, _amount - _fee);
}

  function testExecuteNativeTransferToReceiverZeroAddress(
    address ethSender,
    string memory _id,
    uint256 _amount,
    uint256 _fee,
    address payable _feeReceiver
  ) public {
    /* ------------------------------- preparation ------------------------------ */
    vm.assume(_feeReceiver != address(deFiSmartTransfer));
    vm.assume(_feeReceiver != address(ethSender));
    vm.assume(_feeReceiver != address(0));
    vm.assume(ethSender != address(0));
    vm.assume(_amount != 0);
    _fee = bound(_fee, 0, _amount);
    _notFoundryReservedAddress(_feeReceiver);
    uint256 balanceReceiverBefore = address(0).balance;
    uint256 balanceFeeReceiverBefore = address(_feeReceiver).balance;
    uint256 amountForReceiver = _amount - _fee;

    deFi.Transfer memory _transfer = deFi.Transfer({
      id: _id,
      amount: _amount,
      fee: _fee,
      feeReceiver: _feeReceiver,
      receiver: payable(address(0)),
      payer: ethSender,
      token: address(0)
    });

    /* -------------------------------- execution ------------------------------- */
    hoax(ethSender, _amount);
    assertEq(ethSender.balance, _amount);
    deFiSmartTransfer.executeNativeTransfer{value: _amount}(_transfer);
    assertEq(ethSender.balance, 0);

    /* ------------------------------ verification ------------------------------ */
    uint256 balanceReceiverAfter = address(0).balance;
    uint256 balanceFeeReceiverAfter = address(_feeReceiver).balance;

    assertEq(balanceReceiverAfter, balanceReceiverBefore + amountForReceiver);
    assertEq(balanceFeeReceiverAfter, balanceFeeReceiverBefore + _fee);
  }

  function testExecuteNativeTransferToFeeReceiverZeroAddress(
    address ethSender,
    string memory _id,
    uint256 _amount,
    uint256 _fee,
    address payable _receiver
  ) public {
    /* ------------------------------- preparation ------------------------------ */
    vm.assume(_receiver != address(deFiSmartTransfer));
    vm.assume(_receiver != address(ethSender));
    vm.assume(_receiver != address(0));
    vm.assume(ethSender != address(0));
    vm.assume(_amount != 0);
    _fee = bound(_fee, 0, _amount);
    _notFoundryReservedAddress(_receiver);
    uint256 balanceReceiverBefore = address(_receiver).balance;
    uint256 balanceFeeReceiverBefore = address(0).balance;
    uint256 amountForReceiver = _amount - _fee;

    deFi.Transfer memory _transfer = deFi.Transfer({
      id: _id,
      amount: _amount,
      fee: _fee,
      feeReceiver: payable(address(0)),
      receiver: _receiver,
      payer: ethSender,
      token: address(0)
    });

    /* -------------------------------- execution ------------------------------- */
    hoax(ethSender, _amount);
    assertEq(ethSender.balance, _amount);
    deFiSmartTransfer.executeNativeTransfer{value: _amount}(_transfer);
    assertEq(ethSender.balance, 0);

    /* ------------------------------ verification ------------------------------ */
    uint256 balanceReceiverAfter = address(_receiver).balance;
    uint256 balanceFeeReceiverAfter = address(0).balance;

    assertEq(balanceReceiverAfter, balanceReceiverBefore + amountForReceiver);
    assertEq(balanceFeeReceiverAfter, balanceFeeReceiverBefore + _fee);
  }
BVSS
Recommendation

Add a check to ensure that the receiver and the feeReceiver are not address(0).

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commits 76c823f and cea7f6f by following the mentioned recommendation and adding a check to verify that the receiver and the feeReceiver are not address(0).

Remediation Hash
References
CeFiSmartTransfer.sol#L57-L70
DeFiSmartTransfer.sol#L26-L43

7.4 Lack of input validation for amount and fee values

// Low

Description

Both CeFiSmartTransfer and DeFiSmartTransfer smart contracts allow users to transfer a specific amount of assets to a receiver and a fee amount to a feeReceiver.

Nevertheless, these contracts lack input validation for the amount and fee parameters. Such oversight can result in the submission of transactions with invalid amounts, such as an amount set to 0, or a fee being greater than the amount to transfer, which would revert the transactions.

The affected functions are:

  • CeFiSmartTransfer: prepareNativeTransfer(), executeErc20Transfer() and receive().

  • DeFiSmartTransfer: executeErc20Transfer() and executeNativeTransfer().

Additionally, there is no threshold for minimum and/or maximum amounts, which can allow malicious actors to congest the network with 0 amounts of dust transactions, particularly via DeFiSmartTransfer::executeErc20Transfer(), given that any user could initiate any number of transactions with msg.value = 0.

Proof of Concept
function testReceiveNativeTransferFailsWithFeeIsHigherThanAmount(
    address ethSender,
    string memory _id,
    uint256 _amount,
    uint256 _fee,
    address payable _feeReceiver,
    address payable _receiver
  ) public {

    vm.assume(_feeReceiver != address(ceFiSmartTransfer));
    vm.assume(_receiver != address(ceFiSmartTransfer));
    vm.assume(_amount != 0);
    vm.assume(_amount != type(uint256).max);
    _fee = bound(_fee, _amount + 1, type(uint256).max);
    _notFoundryReservedAddress(_receiver);
    _notFoundryReservedAddress(_feeReceiver);

    assert(_fee > _amount);

    testPrepareNativeTransfer(_id, _amount, _fee, _feeReceiver, _receiver);

    hoax(ethSender, _fee);
    vm.expectRevert();
    (bool ok, ) = address(ceFiSmartTransfer).call{value: _amount}("");
    ok;
}

function testPrepareNativeTransfer(
    string memory _id,
    uint256 _amount,
    uint256 _fee,
    address payable _feeReceiver,
    address payable _receiver
  ) public {
    vm.startPrank(operator1);
    assertEq(uint8(ceFiSmartTransfer.nativeTransferLock()), uint8(CeFi.Lock.Unlocked));

    CeFi.NativeTransfer memory _nativeTransfer = CeFi.NativeTransfer({
      id: _id,
      amount: _amount,
      fee: _fee,
      feeReceiver: payable(_feeReceiver),
      receiver: payable(_receiver)
    });

    ceFiSmartTransfer.prepareNativeTransfer(_nativeTransfer);

    (string memory id_, uint amount_, uint fee_, address feeReceiver_, address receiver_) = ceFiSmartTransfer.nativeTransfer();

    assertEq(_id, id_);
    assertEq(_amount, amount_);
    assertEq(_fee, fee_);
    assertEq(_feeReceiver, feeReceiver_);
    assertEq(_receiver, receiver_);

    assertEq(uint8(ceFiSmartTransfer.nativeTransferLock()), uint8(CeFi.Lock.Locked));

    vm.stopPrank();
}
BVSS
Recommendation

Add checks to ensure that the fee is lower than the amount to transfer, and that the amount to transfer is greater than 0.

Also, consider disallowing transactions below a certain threshold to maintain efficiency and prevent denial of service through dust spamming.

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commits a790ac9 and cea7f6f by following the mentioned recommendations.

Remediation Hash
References
CeFiSmartTransfer.sol#L57-L107
DeFiSmartTransfer.sol#L26-L66

7.5 Operator could front-run a transfer and modify the NativeTransfer data with unexpected fields

// Low

Description

The receive() function within the CeFiSmartTransfer contract is designed to automatically forward all received native tokens to addresses specified in the nativeTransfer struct. This process is initiated when an address sends native assets to the contract, thereby triggering the receive() function. Crucially, this function relies on the prior execution of prepareNativeTransfer() by an operator to set up the transfer parameters.

The contract's current design exposes it to a risk of front-running. An operator could strategically (by monitoring pending transactions in the mempool) or accidentally execute prepareNativeTransfer() before the receive function has been triggered. This would allow the operator to modify the nativeTransfer data, potentially redirecting funds to unauthorized addresses and resulting in a direct financial loss for the legitimate transaction sender. Consider the following scenario:

  • Alice sends ETH to the contract to execute a transfer based on current nativeTransfer values.

  • Operator A calls prepareNativeTransfer() with a higher gas fee to execute the transaction prior to Alice's, modifying the of the receiver and feeReceiver to its address.

  • All ETH sent by Alice goes to the malicious operator address.

Proof of Concept
  function testFrontrunReceiveNativeTransfer(
    string memory _id,
    uint256 _amount,
    uint256 _fee,
    address payable _feeReceiver,
    address payable _receiver
  ) public {
    /* ------------------------------- preparation ------------------------------ */
    vm.assume(_feeReceiver != address(ceFiSmartTransfer));
    vm.assume(_receiver != address(ceFiSmartTransfer));
    vm.assume(_feeReceiver != address(operator1));
    vm.assume(_receiver != address(operator1));
    vm.assume(_amount != 0);
    _fee = bound(_fee, 0, _amount);
    _notFoundryReservedAddress(_receiver);
    _notFoundryReservedAddress(_feeReceiver);
    uint256 balanceReceiverBefore = address(_receiver).balance;
    uint256 balanceFeeReceiverBefore = address(_feeReceiver).balance;

    /* -------------------------------- execution ------------------------------- */
    testPrepareNativeTransfer(_id, _amount, _fee, _feeReceiver, _receiver); // Original native transfer
    _operator1FrontrunAlice(_amount); // Operator1 modifies native transfer
    hoax(alice, _amount);
    (bool ok, ) = (address(ceFiSmartTransfer)).call{value: _amount}("");
    assert(ok);

    /* ------------------------------ verification ------------------------------ */
    uint256 balanceReceiverAfter = address(_receiver).balance;
    uint256 balanceFeeReceiverAfter = address(_feeReceiver).balance;
    assertEq(balanceReceiverAfter, balanceReceiverBefore);
    assertEq(balanceFeeReceiverAfter, balanceFeeReceiverBefore);

    assertEq(address(operator1).balance, _amount);
  }

  function _operator1FrontrunAlice(uint256 _amount) internal {
    testPrepareNativeTransfer("Frontrunning", _amount, 0, payable(operator1), payable(operator1));
  }

  function testPrepareNativeTransfer(
    string memory _id,
    uint256 _amount,
    uint256 _fee,
    address payable _feeReceiver,
    address payable _receiver
  ) public {
    vm.startPrank(operator1);

    CeFi.NativeTransfer memory _nativeTransfer = CeFi.NativeTransfer({
      id: _id,
      amount: _amount,
      fee: _fee,
      feeReceiver: payable(_feeReceiver),
      receiver: payable(_receiver)
    });

    ceFiSmartTransfer.prepareNativeTransfer(_nativeTransfer);

    (string memory id_, uint amount_, uint fee_, address feeReceiver_, address receiver_) = ceFiSmartTransfer.nativeTransfer();

    assertEq(_id, id_);
    assertEq(_amount, amount_);
    assertEq(_fee, fee_);
    assertEq(_feeReceiver, feeReceiver_);
    assertEq(_receiver, receiver_);

    assertEq(uint8(ceFiSmartTransfer.nativeTransferLock()), uint8(CeFi.Lock.Locked));

    vm.stopPrank();
  }
BVSS
Recommendation

A possible recommendation to solve this issue would be to introduce a time lock mechanism that enforces a minimum delay between the execution of prepareNativeTransfer() and its subsequent transactions. This delay would provide a window for any irregularities to be identified and addressed before the transfer is executed.

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit eda3810 by creating a timelock mechanism that waits for at least 1 block between preparation and execution of native transfers.

Remediation Hash
References
CeFiSmartTransfer.sol#L73-L92

7.6 Non-compliant tokens might be permanently locked in contracts

// Low

Description

The CeFiSmartTransfer and the DeFiSmartTransfer contracts assume that all ERC20 token transfers are compliant with the IERC20 standard, which is not accurate. For example, USDT's transfer and transferFrom functions do not return a bool:

/**
* @dev transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address to, uint value) public onlyPayloadSize(2 * 32) {
    uint fee = (_value.mul(basisPointsRate)).div(10000);
    if (fee > maximumFee) {
        fee = maximumFee;
    }
    uint sendAmount = _value.sub(fee);
    balances[msg.sender] = balances[msg.sender].sub(_value);
    balances[_to] = balances[_to].add(sendAmount);
    if (fee > 0) {
      balances[owner] = balances[owner].add(fee);
      Transfer(msg.sender, owner, fee);
    }
    Transfer(msg.sender, _to, sendAmount);
}
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint the amount of tokens to be transferred
*/
function transferFrom(address _from, address _to, uint _value) public onlyPayloadSize(3 * 32) {
    var _allowance = allowed[_from][msg.sender];

    uint fee = (_value.mul(basisPointsRate)).div(10000);
    if (fee > maximumFee) {
        fee = maximumFee;
    }
    if (_allowance < MAX_UINT) {
       allowed[_from][msg.sender] = _allowance.sub(_value);
    }
    uint sendAmount = _value.sub(fee);
    balances[_from] = balances[_from].sub(_value);
    balances[_to] = balances[_to].add(sendAmount);
    if (fee > 0) {
        balances[owner] = balances[owner].add(fee);
        Transfer(_from, owner, fee);
    }
    Transfer(_from, _to, sendAmount);
}

This and any other non-compliant tokens may become locked, as any transfer attempt via the executeErc20Transfer() function will revert the transaction because of the missing return value.

Besides this common example, there are tokens that revert on transfers with 0 amounts, and fee-on-transfer tokens, that causes the received amount to be lesser than the accounted amount. For example, DGX (Digix Gold Token) and CGT (CACHE Gold) tokens apply transfer fees, and the USDT (Tether) token also has a currently disabled fee feature. For more reference, see here.

Proof of Concept
contract USDTSimplified {
  mapping(address => uint) public balances;

  constructor(uint256 _amount) {
    balances[msg.sender] = _amount;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }

  function transfer(address _to, uint _value) public {
    balances[msg.sender] -= _value;
    balances[_to] += _value;
  }
} 
function testExecuteErc20TransferWithNoBooleanReturnFails(
    string memory _id,
    uint256 _amount,
    uint256 _fee,
    address payable _receiver,
    address payable _feeReceiver
  ) external {
    vm.assume(_amount != 0);
    vm.assume(_receiver != address(0));
    vm.assume(_feeReceiver != address(0));

    _fee = bound(_fee, 0, _amount);
    _notFoundryReservedAddress(_receiver);
    _notFoundryReservedAddress(_feeReceiver);

    vm.startPrank(deployer);
    USDTSimplified usdt = new USDTSimplified(_amount);
    assertEq(usdt.balanceOf(deployer), _amount);

    usdt.transfer(address(ceFiSmartTransfer), _amount);
    assertEq(usdt.balanceOf(deployer), 0);
    assertEq(usdt.balanceOf(address(ceFiSmartTransfer)), _amount);

    CeFi.Erc20Transfer memory _erc20Transfer = CeFi.Erc20Transfer({
      id: _id,
      amount: _amount,
      fee: _fee,
      feeReceiver: payable(alice),
      receiver: payable(bob),
      token: address(usdt)
    });

    vm.expectRevert();
    ceFiSmartTransfer.executeErc20Transfer(_erc20Transfer);
}
BVSS
Recommendation

It is recommended to use OpenZeppelin’s SafeERC20 library to handle most edge cases among ERC20 tokens, including the return value check, to avoid silently failing transfers that could result in a partial or total loss of the users' investment.

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit 284e2ab by using OpenZeppelin’s SafeERC20 library and its proper implementation.

Remediation Hash
References
CeFiSmartTransfer.sol#L99-L103
DeFiSmartTransfer.sol#L51-L61

7.7 Id fields for transfers are not unique

// Informational

Description

Comments in the code inside the CeFiSmartTransfer and DeFiSmartTransfer contracts declare that the id field in the NativeTransfer , Erc20Transfer and Transfer structs represents a unique identifier for the transfer.

string id; // A unique identifier for the transfer.

This is not accurate, as the contents of the nativeTransfer variable can be replaced anytime by an operator calling the prepareNativeTransfer function, effectively making it possible to have two different sets of nativeTransfer data sets with the same id.

Similarly, the contents of Erc20Transfer in CeFiSmartTransfer and Transfer in DeFiSmartTransfer are declared via input when calling executeErc20Transfer, making it possible to re-use a previously used id.

This could affect the data collected when monitoring the aforementioned contracts, since the id field is used in the emission of SmartTransferReplaced , SmartTransferComplete and SmartTransferReady events.

BVSS
Recommendation

Keep a record of declared and/or used IDs and place a check to ensure that an ID cannot be reused in different transfers.

Remediation Plan

ACKNOWLEDGED: The Mesh Connect team acknowledged this finding, stating: We will apply off-chain considerations to ensure that transfer IDs are unique.

References
CeFiSmartTransfer.sol#L9
CeFiSmartTransfer.sol#L57-L70
CeFiSmartTransfer.sol#L95-L107
DeFiSmartTransfer.sol#L13
DeFiSmartTransfer.sol#L26-L66

7.8 Check-effects-interaction pattern is not followed

// Informational

Description

The receive() function within the smart contract allows users to transfer to forward the native asset to the designated receiver address and handling the transfer of the fee to the feeReceiver. Following these operations, the contract sets the nativeTransferLock state variable to Unlocked, to allow for a new native transfer to be initiated. However, the nativeTransferLock value is set to Unlocked after the transfer has been made, which is a state-changing operation.

This implementation does not follow the recommended Check-Effects-Interactions pattern. According to this pattern, any modifications to the contract's state should precede calls to external contracts or addresses. While the current usage of the native transfer method mitigates the immediate risk of reentrancy attacks, substituting transfer with the lower-level call could introduce such vulnerabilities. Hence, adherence to the Check-Effects-Interactions pattern remains a best practice, ensuring enhanced security and future-proofing the contract against potential reentrancy threats.

Score
Recommendation

Update the nativeTransferLock to an Unlocked state before executing any transfers. This ensures compliance with the Check-Effects-Interactions pattern, enhancing contract security.

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit 2e5eef7 by following the recommendation and updating the state prior to executing transfers.

Remediation Hash
References
CeFiSmartTransfer.sol#L73-L92

7.9 Transfer identifier could be stored with a cheaper value type

// Informational

Description

In Solidity, using strings is not the most gas efficient way to handle data, since they are stored as a dynamic array.

If the length is 32 bytes or longer, the slot in which they are defined stores the length of the string * 2 + 1, while their actual data is stored elsewhere (the keccak256 hash of that slot).

However, if a string is less than 32 bytes, the length * 2 is stored at the least significant byte of it’s storage slot and the actual data of the string is stored starting from the most significant byte in the slot in which it is defined.

Score
Recommendation

Consider replacing the id field of the NativeTransfer struct to a bytes32 type.

Storing and manipulating data in bytes32 is more gas-efficient than using string, which involves dynamic storage allocation.

Alternatively, the id field could be stored as a uint96 type value, to be able to pack the value with an address of the same struct (feeReceiver or receiver) and reduce the usage of a storage slot.

Nevertheless, limit the size of the string input to keep it under 32 bytes.

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit 2e5eef7 by following the recommendation and storing the id field into a bytese32 type.

Remediation Hash

7.10 Redundant code

// Informational

Description

In the constructor of CeFiSmartTransfer.sol, there is an initialization attempt for the nativeTransfer variable. This initialization is unneeded and impractical, since this NativeTransfer values will not and can not be used until the prepareNativeTransfer() function is called, where the operators will be able to set the NativeTransfer values.

Additionally, the values of the nativeTransfer variable initialized in the constructor should not be valid params, having the amount and fee be 1 wei and both the feeReceiver and receiver being the address(1).

Follow-up assessment:

In the follow-up assessment, it was identified that there is a redundant initialization in the constructor of the CeFiSmartTransfer contract, where the newly declared isLocked variable is initialized to false. This is unnecessary, as boolean type variable are initialized as false by default in Solidity.

Additionally, in the executeNativeTransfer function from the DefiSmartTransfer smart contract, there is a check to ensure the fee is valid:

if ((transfer.fee < 0) || (transfer.fee > transfer.amount)) revert InvalidFeeAmount({fee: transfer.fee, amount: transfer.amount});

Here, the (transfer.fee < 0) code fragment can be omitted, given that the this condition will never be true because uint256 type variables can't store negative values.

Score
Recommendation

Remove the aforementioned fragments of code.

Remediation Plan (1st assessment)

ACKNOWLEDGED: The Mesh Connect team acknowledged this finding and stated that: Maintaining nativeTransfer initialization for gas use consistency will reduce gas consumption across different test scenarios.

Remediation Plan (2nd assessment)

ACKNOWLEDGED: The Mesh Connect team has solved the finding by following the mentioned recommendations.

Remediation Hash
References
CeFiSmartTransfer.sol#L47-L53
CeFiSmartTransfer.sol#L66
DeFiSmartTransfer.sol#L64

7.11 Unoptimized for loop declaration

// Informational

Description

There is an instance of an unoptimized for loop that may incur in higher gas costs than necessary.

Score
Recommendation

Optimize the instance of a for loop, by not initializing i to its default value of 0, using the pre-increment operator and unchecked code blocks for the loop counter.

for (uint256 i; i < loopAmount;) {
// code logic
unchecked { ++i; }
}

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit 2e5eef7 by following the mentioned recommendation.

Remediation Hash
References
Operator.sol#L13-L15

7.12 Public functions not called within the contract can be made external to save gas

// Informational

Description

The Operator.sol contract currently defines the addOperator() and removeOperator() functions with public visibility, even though they are not called from within the smart contract, resulting in higher gas costs than necessary.

Score
Recommendation

Modify the aforementioned functions with the external visibility modifier.

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit 2e5eef7 by following the mentioned recommendation.

Remediation Hash
References
Operator.sol#L25
Operator.sol#L30

7.13 Unlocked pragma compilers

// Informational

Description

Contracts should be deployed with the same compiler version and flags used during development and testing. Locking the pragma helps to ensure that contracts do not accidentally get deployed using another pragma. For example, an outdated pragma version might introduce bugs that affect the contract system negatively.

Score
Recommendation

Lock the pragma version to the same version used during development and testing.

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit 2e5eef7 by following the mentioned recommendation.

Remediation Hash
References
CeFiSmartTransfer.sol#L2
DeFiSmartTransfer.sol#L2
Operator.sol#L2

7.14 Use of custom errors instead of revert strings may help reduce gas usage

// Informational

Description

In Solidity smart contract development, replacing hard-coded revert message strings with the Error() syntax is an optimization strategy that can significantly reduce gas costs. Hard-coded strings, stored on the blockchain, increase the size and cost of deploying and executing contracts.

The Error() syntax allows for the definition of reusable, parameterized custom errors, leading to a more efficient use of storage and reduced gas consumption. This approach not only optimizes gas usage during deployment and interaction with the contract but also enhances code maintainability and readability by providing clearer, context-specific error information.

Score
Recommendation

Consider replacing all revert strings with custom errors. For more reference, see here.

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit 2e5eef7 by following the mentioned recommendation.

Remediation Hash
References
Operator.sol#L20
CeFiSmartTransfer.sol#L75-L83
DeFiSmartTransfer.sol#L27-L34

7.15 Events are missing the indexed attribute

// Informational

Description

Indexed event fields make the data more quickly accessible to off-chain tools that parse events, and adds them to a special data structure known as "topics" instead of the data part of the log.

However, indexing more fields increases the gas cost for each event emitted. Therefore, extensive indexing is recommended when the advantages of easier data retrieval outweigh the higher gas expenses, particularly in cases where gas efficiency is less of a concern.

Additionally, it's worth noting that when you attempt to index dynamic data types like string in Solidity, which is the case for the contracts in scope, they don't get stored in their original form. Instead, it is stored the Keccak-256 hash of these data types, so to search for a specific string in the logs, developers would have to hash their desired string using Keccak-256 and then search for that resultant hash among the indexed parameters.

Score
Recommendation

If the value to emit in an event is fix-sized, it is recommended to add the indexed keyword when declaring events. For more reference, see the Solidity documentation.

Remediation Plan

ACKNOWLEDGED: The Mesh Connect team has made a business decision to acknowledge this finding and not alter the contracts.

Remediation Hash
References
CeFiSmartTransfer.sol#L36-L40
DeFiSmartTransfer.sol#L23

7.16 Unused struct fields when calling executeNativeTransfer function

// Informational

Description

The payer field in the Transfer struct is not used when calling the executeNativeTransfer() function. Additionally the token field is required to be the address(0) to ensures that the transfer is for a native token.

Score
Recommendation

Implement a different struct type for native token transfers that does not require a token field or a payer field.

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit 2e5eef7 by following the mentioned recommendation.

Remediation Hash
References
DeFiSmartTransfer.sol#L26-L43

7.17 Explicit imports

// Informational

Description

Throughout the codebase, the approach to integrating external code involved importing full files, eg:

import "./Operator.sol";

Score
Recommendation

It's considered best practice to follow named imports instead of importing full files, for example:

import {Operator} from "./Operator.sol";

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit 1685f45 by following the mentioned recommendation.

Remediation Hash

7.18 Transfer with hardcoded gas amount

// Informational

Description

The receive() function from CeFiSmartTransfer and the executeNativeTransfer() function from DeFiSmartTransfer use the native transfer() method to send native assets (e.g. ETH) calling from the smart contract.

The transfer() and send() functions forward a fixed amount of 2300 gas. Historically, it has often been recommended to use these functions for value transfers to guard against reentrancy attacks. However, the gas cost of EVM instructions may change significantly during hard forks which may break already deployed contract systems that make fixed assumptions about gas costs. For example, EIP 1884 broke several existing smart contracts due to a cost increase of the SLOAD instruction.

Score
Recommendation

Avoid the use of transfer() and send() and do not otherwise specify a fixed amount of gas when performing calls. Use .call.value(...)("") instead. Use the checks-effects-interactions pattern and/or reentrancy locks to prevent reentrancy attacks to the system when executing these calls. For more reference see here.

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit 2e5eef7 by following the mentioned recommendation.

Remediation Hash
References
CeFiSmartTransfer.sol#L85-L88
DeFiSmartTransfer.sol#L37-L39

7.19 PUSH0 is not supported by all chains

// Informational

Description

The compiler for Solidity 0.8.20 switches the default target EVM version to Shanghai, which means that the generated bytecode will include PUSH0 opcodes. Be sure to select the appropriate EVM version in case you intend to deploy on a chain other than mainnet like L2 chains that may not support PUSH0, otherwise deployment of your contracts will fail.

Score
Recommendation

Make sure to specify the target EVM version when using Solidity 0.8.20, especially if deploying to L2 chains that may not support the PUSH0 opcode. Stay informed about the opcode support of different chains to ensure smooth deployment and compatibility.

Remediation Plan

ACKNOWLEDGED: The Mesh Connect team acknowledged this finding stating: We will apply off-chain considerations to ensure the target EVM supports PUSH0.

7.20 Lack of input validation for self-transfers

// Informational

Description

The receive() function of CeFiSmartTransfer and the executeNativeTransfer() function of DeFiSmartTransfer lack checks to verify that the sender of the asset and the receiver and/or feeReceiver are not the same.


Score
Recommendation

Add a check to verify that the address of the transaction sender is not the same as receiver and feeReceiver.

Remediation Plan

ACKNOWLEDGED: The Mesh Connect team has made a business decision to acknowledge this finding and not alter the contracts.

Remediation Hash
References
CeFiSmartTransfer.sol#L73-L92
DeFiSmartTransfer.sol#L26-L43

7.21 Variable declaration consistency

// Informational

Description

It has been identified that there is inconsistency in the uint256 type declaration in DeFiSmartTransfer and CeFiSmartTransfer contracts. Some of these variables are declared using the uint alias, while others are declared using uint256. While this won't affect the functionality of the contracts, it is a good practice to use the same explicit type declaration for all variables.

Score
Recommendation

Change all uint type declarations to uint256 type declarations.

Remediation Plan

SOLVED: The Mesh Connect team solved this finding in commit 54d9498 by following the mentioned recommendation.

Remediation Hash
References
CeFiSmartTransfer.sol#L16-L17
CeFiSmartTransfer.sol#L26-L27
CeFiSmartTransfer.sol#L50
CeFiSmartTransfer.sol#L52
CeFiSmartTransfer.sol#L53
CeFiSmartTransfer.sol#L55
CeFiSmartTransfer.sol#L58-L60
DeFiSmartTransfer.sol#L19-L20
DeFiSmartTransfer.sol#L28-L29
CeFiSmartTransfer.sol#L40
CeFiSmartTransfer.sol#L42
CeFiSmartTransfer.sol#L43
CeFiSmartTransfer.sol#L45

8. Automated Testing

Static Analysis Report

Description

Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contracts in scope. Among the tools used was Slither, a Solidity 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, Slither was run against the contracts. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contracts' APIs across the entire code-base.

The security team assessed all findings identified by the Slither software, however, findings with related to external dependencies are not included in the below results for the sake of report readability.

Output

The findings obtained as a result of the Slither scan were reviewed, and taken into consideration for the report. Some were not included because they were determined as false positives.

Mesh - slither results

Unit tests and fuzzing

The original repository used the Hardhat environment to develop and test the smart contracts. All tests were executed successfully. Additionally, the project in scope was cloned to a Foundry environment, to allow for additional testing and fuzz testing that covered ~50,000 runs per test. These additional tests were ran successfully.

Mesh - Fuzz testing

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