Halborn Logo

Cross Chain Messaging Protocol - Biconomy


Prepared by:

Halborn Logo

HALBORN

Last Updated 04/25/2024

Date of Engagement by: January 26th, 2023 - February 23rd, 2023

Summary

0% of all REPORTED Findings have been addressed

All findings

7

Critical

1

High

0

Medium

2

Low

1

Informational

3


1. INTRODUCTION

Biconomy engaged Halborn to conduct a security audit on their smart contracts beginning on 2023-01-26 and ending on 2023-02-23. The security assessment was scoped to the smart contracts provided to the Halborn team.

2. AUDIT SUMMARY

The team at Halborn assigned a full-time security engineer to audit the security of the smart contracts. 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 audit is to:

    • Ensure that smart contract functions operate as intended

    • Identify potential security issues with the smart contracts

In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which should be addressed by Biconomy team. The main ones are the following:

    • Update the logic of 'addGasFee' function to allow tokens transfer only from msg.sender.

    • Revert the transactions if users try to add gas fees when a gateway is paused.

    • Validate that ownership transfer to CCMPExecutor is not a reachable scenario before applying any change.

    • Split ownership transfer functionality to allow the recipient to complete the transfer.

3. TEST APPROACH & 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 audit. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of the 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 audit:

    • Research into architecture and purpose.

    • Smart contract manual code review and walkthrough.

    • Graphing out functionality and contract logic/connectivity/functions (solgraph)

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

    • Manual testing by custom scripts.

    • Scanning of solidity files for vulnerabilities, security hotspots or bugs (MythX).

    • Static Analysis of security for scoped contract, and imported functions (Slither).

    • Testnet deployment (Remix IDE).

4. SCOPE

. Solidity Smart Contracts

#. Repository: [ccmp-contracts ](https://github.com/bcnmy/ccmp-contracts)
#. Commit ID: [82c7cfb](https://github.com/bcnmy/ccmp-contracts/tree/82c7cfb27540b31dbf49a563fd0acdbbd50074ae)
#. Contract in scope:
    - adaptors/base/AbacusConnectionClient.sol
    - adaptors/base/CCMPAdaptorBase.sol
    - adaptors/AxelarAdaptor.sol
    - adaptors/HyperlaneAdaptor.sol
    - adaptors/WormholeAdaptor.sol
    - gateway/facets/CCMPConfigurationFacet.sol
    - gateway/facets/CCMPReceiveMessageFacet.sol
    - gateway/facets/CCMPSendMessageFacet.sol
    - gateway/facets/DiamondCutFacet.sol
    - gateway/facets/DiamondInit.sol
    - gateway/facets/DiamondLoupeFacet.sol
    - gateway/Diamond.sol
    - libraries/LibDiamond.sol
    - receiver/CCMPReceiver.sol
    - receiver/CCMPReceiverBase.sol
    - receiver/CCMPReceiverUpgradeable.sol
    - security/Pausable.sol
    - CCMPExecutor.sol

Out-of-scope: External libraries and financial related attacks.

5. RISK METHODOLOGY

Vulnerabilities or issues observed by Halborn are ranked based on the risk assessment methodology by measuring the LIKELIHOOD of a security incident and the IMPACT should an incident occur. This framework works for communicating the characteristics and impacts of technology vulnerabilities. The quantitative model ensures repeatable and accurate measurement while enabling users to see the underlying vulnerability characteristics that were used to generate the Risk scores. For every vulnerability, a risk level will be calculated on a scale of 5 to 1 with 5 being the highest likelihood or impact.
RISK SCALE - LIKELIHOOD
  • 5 - Almost certain an incident will occur.
  • 4 - High probability of an incident occurring.
  • 3 - Potential of a security incident in the long term.
  • 2 - Low probability of an incident occurring.
  • 1 - Very unlikely issue will cause an incident.
RISK SCALE - IMPACT
  • 5 - May cause devastating and unrecoverable impact or loss.
  • 4 - May cause a significant level of impact or loss.
  • 3 - May cause a partial impact or loss to many.
  • 2 - May cause temporary impact or loss.
  • 1 - May cause minimal or un-noticeable impact.
The risk level is then calculated using a sum of these two values, creating a value of 10 to 1 with 10 being the highest level of security risk.
Critical
High
Medium
Low
Informational
  • 10 - CRITICAL
  • 9 - 8 - HIGH
  • 7 - 6 - MEDIUM
  • 5 - 4 - LOW
  • 3 - 1 - VERY LOW AND INFORMATIONAL

6. SCOPE

Out-of-Scope: New features/implementations after the remediation commit IDs.

7. Assessment Summary & Findings Overview

Critical

1

High

0

Medium

2

Low

1

Informational

3

Impact x Likelihood

HAL-03

HAL-01

HAL-04

HAL-02

HAL-05

HAL-06

HAL-07

Security analysisRisk levelRemediation Date
INADEQUATE ACCESS CONTROL IN ADDGASFEE FUNCTION ALLOWS TO STEAL USERS' TOKENSCriticalNot Solved - 06/30/2023
PAUSED GATEWAYS DO NOT PREVENT USERS FROM ADDING GAS FEESMediumNot Solved - 06/30/2023
TRANSFERRING OWNERSHIP TO CCMPEXECUTOR LEADS TO A TAKEOVER OF CCMP CONTRACTSMediumNot Solved - 06/30/2023
OWNERSHIP OF GATEWAYS CAN BE TRANSFERRED WITHOUT CONFIRMATIONLowNot Solved - 06/30/2023
TOKENS CAN BE REPEATED WHEN QUERYING GAS FEE PAYMENT DETAILSInformationalNot Solved - 06/30/2023
MISSING ZERO ADDRESS CHECKInformationalNot Solved - 06/30/2023
UNUSED FUNCTIONSInformationalNot Solved - 06/30/2023

8. Findings & Tech Details

8.1 INADEQUATE ACCESS CONTROL IN ADDGASFEE FUNCTION ALLOWS TO STEAL USERS' TOKENS

// Critical

Description

An attacker can use addGasFee function in CCMPSendMessageFacet contract and send a custom GasFeePaymentArgs to force the transfer of ERC20 tokens from all the users, who previously approved CCMPGateway to manage their tokens, to a fake relayer (i.e.: the attacker himself). The following situations increase the risk level of this issue:

  • The attack can be replicated in all the chains where there is a gateway and allows stealing all the tokens approved, even if they were expected to be spent on later transactions yet.
  • Even if the approval and the deposit of tokens is done together, this last transaction could be front-runned if necessary.

Here is a proof of concept showing how to exploit this security issue:

Proof of Concept:

  1. A user approves that CCMPGateway manages 1_000_000 tokens.

  2. An attacker creates a custom GasFeePaymentArgs to steal all the tokens from the user.

  3. The attacker calls the addGasFee function with the custom GasFeePaymentArgs. After this transaction, the balance of the attacker should have increased by 1_000_000 and the balance of the user should be 0.

  4. Finally, the attack is successful, as shown in the test.

Code Location

gateway/facets/CCMPSendMessageFacet.sol

function addGasFee(
  GasFeePaymentArgs memory _args,
  bytes32 _messageHash,
  address _sender
) public payable {
  uint256 feeAmount = _args.feeAmount;
  address relayer = _args.relayer;
  address tokenAddress = _args.feeTokenAddress;

  LibDiamond.CCMPDiamondStorage storage ds = LibDiamond._diamondStorage();

  if (feeAmount > 0) {
    ds.gasFeePaidByToken[_messageHash][tokenAddress][
      relayer
    ] += feeAmount;

    if (tokenAddress == NATIVE_ADDRESS) {
      if (msg.value != feeAmount) {
        revert NativeAmountMismatch();
      }
      (bool success, bytes memory returndata) = relayer.call{
        value: msg.value
      }("");
      if (!success) {
        revert NativeTransferFailed(relayer, returndata);
      }
    } else {
      if (msg.value != 0) {
        revert NativeAmountMismatch();
      }
      IERC20(tokenAddress).safeTransferFrom(
        _sender,
        relayer,
        feeAmount
      );
    }

    emit FeePaid(tokenAddress, feeAmount, relayer);
  }
}
Score
Impact: 5
Likelihood: 5

8.2 PAUSED GATEWAYS DO NOT PREVENT USERS FROM ADDING GAS FEES

// Medium

Description

addGasFee function in CCMPSendMessageFacet contract allows users to add gas fee even if a gateway is paused, which would go against the business logic of the protocol.

As a consequence, users could lose their recently deposited tokens if a gateway has been paused because of an unexpected scenario: cyberattacks, market adverse conditions, security issues with the adaptors, etc.

Code Location

gateway/facets/CCMPSendMessageFacet.sol

function addGasFee(
  GasFeePaymentArgs memory _args,
  bytes32 _messageHash,
  address _sender
) public payable {
  uint256 feeAmount = _args.feeAmount;
  address relayer = _args.relayer;
  address tokenAddress = _args.feeTokenAddress;

  LibDiamond.CCMPDiamondStorage storage ds = LibDiamond._diamondStorage();

  if (feeAmount > 0) {
    ds.gasFeePaidByToken[_messageHash][tokenAddress][
      relayer
    ] += feeAmount;

    if (tokenAddress == NATIVE_ADDRESS) {
      if (msg.value != feeAmount) {
        revert NativeAmountMismatch();
      }
      (bool success, bytes memory returndata) = relayer.call{
        value: msg.value
      }("");
      if (!success) {
        revert NativeTransferFailed(relayer, returndata);
      }
    } else {
      if (msg.value != 0) {
        revert NativeAmountMismatch();
      }
      IERC20(tokenAddress).safeTransferFrom(
        _sender,
        relayer,
        feeAmount
      );
    }

    emit FeePaid(tokenAddress, feeAmount, relayer);
  }
}
Score
Impact: 4
Likelihood: 3

8.3 TRANSFERRING OWNERSHIP TO CCMPEXECUTOR LEADS TO A TAKEOVER OF CCMP CONTRACTS

// Medium

Description

transferOwnership and setCCMPExecutor functions do not validate the new values of the contract owner and CCMPExecutor respectively before making the changes. For instance, if the ownership of a gateway is mistakenly transferred to CCMPExecutor, any user will be able to call restricted functions on CCMPConfigurationFacet contract.

As a consequence, a malicious user could modify gateways, router adaptors and pausers in order to make a takeover of the CCMP contracts. It is worth noting that the likelihood of this issue being triggered is very limited because the functions mentioned above can be called only by the contract owner.

Code Location

gateway/facets/CCMPConfigurationFacet.sol

function transferOwnership(
  address _newOwner
) external override(IERC173, ICCMPConfiguration) {
  LibDiamond._enforceIsContractOwner();
  LibDiamond._setContractOwner(_newOwner);
}

gateway/facets/CCMPConfigurationFacet.sol

function setCCMPExecutor(ICCMPExecutor _ccmpExecutor) external {
  LibDiamond._enforceIsContractOwner();
  LibDiamond._diamondStorage().ccmpExecutor = _ccmpExecutor;
  emit CCMPExecutorUpdated(_ccmpExecutor);
}
Score
Impact: 5
Likelihood: 1

8.4 OWNERSHIP OF GATEWAYS CAN BE TRANSFERRED WITHOUT CONFIRMATION

// Low

Description

An incorrect use of the transferOwnership function in CCMPConfigurationFacet contract can set owner to an invalid address and inadvertently lose control of a gateway, which cannot be undone in any way. Currently, the owner of the contract can change owner address using the aforementioned function in a single transaction and without confirmation from the new address.

Code Location

Ownership transfer for gateways in a single step:

gateway/facets/CCMPConfigurationFacet.sol

function transferOwnership(
  address _newOwner
) external override(IERC173, ICCMPConfiguration) {
  LibDiamond._enforceIsContractOwner();
  LibDiamond._setContractOwner(_newOwner);
}

libraries/LibDiamond.sol

function _setContractOwner(address _newOwner) internal {
  CCMPDiamondStorage storage ds = _diamondStorage();
  address previousOwner = ds.contractOwner;
  ds.contractOwner = _newOwner;
  emit OwnershipTransferred(previousOwner, _newOwner);
}
Score
Impact: 4
Likelihood: 1

8.5 TOKENS CAN BE REPEATED WHEN QUERYING GAS FEE PAYMENT DETAILS

// Informational

Description

If a relayer mistakenly uses the getGasFeePaymentDetails function in CCMPSendMessageFacet contract with a repeated token in _tokens argument, the function will not throw an error message.

This situation could create a potential issue for the relayer if it directly accrues the values of all the tokens by just querying this function because the real value will be different from the calculated one with the function mentioned above.

Code Location

gateway/facets/CCMPSendMessageFacet.sol

function getGasFeePaymentDetails(
 bytes32 _messageHash,
 address[] calldata _tokens,
 address _relayer
) external view returns (uint256[] memory balances) {
 LibDiamond.CCMPDiamondStorage storage ds = LibDiamond._diamondStorage();
 balances = new uint256[](_tokens.length);
 unchecked {
  for (uint256 i = 0; i < _tokens.length; ++i) {
   balances[i] = ds.gasFeePaidByToken[_messageHash][_tokens[i]][
    _relayer
   ];
  }
 }
}
Score
Impact: 2
Likelihood: 1

8.6 MISSING ZERO ADDRESS CHECK

// Informational

Description

There is no validation that the _owner address in the constructor of CCMPAdaptorBase contract - which is then inherited by Axelar, Hyperlane and Wormhole adaptors - is different from zero address.

It is worth noting that the call to _transferOwnership does not validate the address mentioned, either.

Code Location

contracts/adaptors/base/CCMPAdaptorBase.sol

constructor(
  address _ccmpGateway,
  address _owner,
  address _pauser
) PausableBase(_pauser) {
  ccmpGateway = ICCMPGateway(_ccmpGateway);
  _transferOwnership(_owner);
}
Score
Impact: 2
Likelihood: 1

8.7 UNUSED FUNCTIONS

// Informational

Description

setPauser and pauser functions in CCMPConfigurationFacet contract implement their logic and do not reuse some internal functions that do the same tasks. Although it is not a security issue, it is worth noting that this recommendation is more focused on code hygiene and to avoid potential issues in the future if some part of this code is refactored.

Code Location

setPauser function could call _setContractPauser to set the new pauser:

gateway/facets/CCMPConfigurationFacet.sol

function setPauser(address _pauser) external {
  LibDiamond._enforceIsContractOwner();
  LibDiamond._diamondStorage().pauser = _pauser;
  emit PauserUpdated(_pauser);
}

libraries/LibDiamond.sol

function _setContractPauser(address _newPauser) internal {
  _diamondStorage().pauser = _newPauser;
}

\color{black}\color{white}pauser function could call _contractPauser to query the pauser address:

gateway/facets/CCMPConfigurationFacet.sol

function pauser() external view returns (address pauser_) {
  pauser_ = LibDiamond._diamondStorage().pauser;
}

libraries/LibDiamond.sol

function _contractPauser() internal view returns (address pauser_) {
  pauser_ = _diamondStorage().pauser;
}
Score
Impact: 1
Likelihood: 1

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.