Prepared by:
HALBORN
Last Updated 04/25/2024
Date of Engagement by: January 26th, 2023 - February 23rd, 2023
0% of all REPORTED Findings have been addressed
All findings
7
Critical
1
High
0
Medium
2
Low
1
Informational
3
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.
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.
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
).
#. 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.
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 analysis | Risk level | Remediation Date |
---|---|---|
INADEQUATE ACCESS CONTROL IN ADDGASFEE FUNCTION ALLOWS TO STEAL USERS' TOKENS | Critical | Not Solved - 06/30/2023 |
PAUSED GATEWAYS DO NOT PREVENT USERS FROM ADDING GAS FEES | Medium | Not Solved - 06/30/2023 |
TRANSFERRING OWNERSHIP TO CCMPEXECUTOR LEADS TO A TAKEOVER OF CCMP CONTRACTS | Medium | Not Solved - 06/30/2023 |
OWNERSHIP OF GATEWAYS CAN BE TRANSFERRED WITHOUT CONFIRMATION | Low | Not Solved - 06/30/2023 |
TOKENS CAN BE REPEATED WHEN QUERYING GAS FEE PAYMENT DETAILS | Informational | Not Solved - 06/30/2023 |
MISSING ZERO ADDRESS CHECK | Informational | Not Solved - 06/30/2023 |
UNUSED FUNCTIONS | Informational | Not Solved - 06/30/2023 |
// Critical
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:
Here is a proof of concept showing how to exploit this security issue:
Proof of Concept:
A user approves that CCMPGateway manages 1_000_000 tokens
.
An attacker creates a custom GasFeePaymentArgs
to steal all the tokens from the user.
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
.
Finally, the attack is successful, as shown in the test.
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);
}
}
// Medium
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.
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);
}
}
// Medium
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.
function transferOwnership(
address _newOwner
) external override(IERC173, ICCMPConfiguration) {
LibDiamond._enforceIsContractOwner();
LibDiamond._setContractOwner(_newOwner);
}
function setCCMPExecutor(ICCMPExecutor _ccmpExecutor) external {
LibDiamond._enforceIsContractOwner();
LibDiamond._diamondStorage().ccmpExecutor = _ccmpExecutor;
emit CCMPExecutorUpdated(_ccmpExecutor);
}
// Low
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.
Ownership transfer for gateways in a single step:
function transferOwnership(
address _newOwner
) external override(IERC173, ICCMPConfiguration) {
LibDiamond._enforceIsContractOwner();
LibDiamond._setContractOwner(_newOwner);
}
function _setContractOwner(address _newOwner) internal {
CCMPDiamondStorage storage ds = _diamondStorage();
address previousOwner = ds.contractOwner;
ds.contractOwner = _newOwner;
emit OwnershipTransferred(previousOwner, _newOwner);
}
// Informational
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.
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
];
}
}
}
// Informational
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.
constructor(
address _ccmpGateway,
address _owner,
address _pauser
) PausableBase(_pauser) {
ccmpGateway = ICCMPGateway(_ccmpGateway);
_transferOwnership(_owner);
}
// Informational
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.
setPauser
function could call _setContractPauser
to set the new pauser:
function setPauser(address _pauser) external {
LibDiamond._enforceIsContractOwner();
LibDiamond._diamondStorage().pauser = _pauser;
emit PauserUpdated(_pauser);
}
function _setContractPauser(address _newPauser) internal {
_diamondStorage().pauser = _newPauser;
}
\color{black}\color{white}pauser
function could call _contractPauser
to query the pauser address:
function pauser() external view returns (address pauser_) {
pauser_ = LibDiamond._diamondStorage().pauser;
}
function _contractPauser() internal view returns (address pauser_) {
pauser_ = _diamondStorage().pauser;
}
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