Halborn Logo

CCCM EVM V1 - Concrete


Prepared by:

Halborn Logo

HALBORN

Last Updated 10/15/2024

Date of Engagement by: June 21st, 2024 - July 5th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

5

Critical

2

High

0

Medium

0

Low

1

Informational

2


1. Introduction

Concrete engaged Halborn to conduct a security assessment on their smart contract beginning on June 21st, 2024 and ending on July 5th, 2024. A security assessment on smart contracts was performed on the scoped smart contracts provided to the Halborn team.

2. Assessment Summary

The team at Halborn was provided two weeks for the engagement and assigned a full-time security engineer to verify 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 security risks that were successfully addressed by the Concrete team.

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 code and can 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 (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.

    • Testnet deployment (Foundry).

3.1 Out-of-scope

    • External libraries and financial-related attacks.

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: sc_cccm-evm-v1
(b) Assessed Commit ID: 713c636
(c) Items in scope:
  • contracts/SingleMessaging.sol
  • contracts/Registry.sol
  • contracts/OnlyRoleForEndpoint.sol
↓ Expand ↓
Out-of-Scope:
Remediation Commit ID:
  • f04b22d
  • e2882d0
  • 6533fb5
  • 849c576
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

2

High

0

Medium

0

Low

1

Informational

2

Security analysisRisk levelRemediation Date
Packets Error Handling: EVM revert in ABI DecodingCriticalSolved - 07/01/2024
Incorrect Fragment Count Leading to External Call ErrorCriticalSolved - 07/01/2024
Lack of Packet Size LimitationLowSolved - 07/31/2024
Missing nonReentrant modifierInformationalSolved - 08/01/2024
No Events for State ChangesInformationalSolved - 07/27/2024

7. Findings & Tech Details

7.1 Packets Error Handling: EVM revert in ABI Decoding

// Critical

Description

An EVM error is encountered at the following line in the Solidity code: (address target, bytes memory data) = abi.decode(modifiedPacket, (address, bytes));

This error occurs because modifiedPacket does not match the expected encoding for (address, bytes).

function multipleReceivePseudoInternal(
        uint8 numFrgmt_,
        uint8 lastFrgmtCnt_,
        bytes memory instructions_,
        bytes[] memory packets_, // NOTE: Only the packets for this bundle
        bytes[] memory responses_, // all the responses. // the ones that have not yet been generated are empty
        bytes32 successFlags_
    ) external OnlyPseudoInternalCalls returns (bytes[] memory responses) {
        bool callSuccess;
        uint16 startFrom;
        // bool inputFromRevertedCallFlag;
        bytes[] memory newResponses = new bytes[](numFrgmt_);
        bytes memory modifiedPacket;
        for (uint256 i = 0; i < numFrgmt_; i++) {
            (callSuccess, startFrom, modifiedPacket) =
                packets_[i].modifyPacket(responses_, successFlags_, instructions_, startFrom);

            if (!callSuccess) {
                revert EndpointErrors.CannotInsertInput(lastFrgmtCnt_ + uint8(i));
            }

            (address target, bytes memory data) = abi.decode(modifiedPacket, (address, bytes));

            // This external call should just execute the calldata that is passed to it. If that call data is a zero
            // address, then so it should be.
            // slither-disable-next-line missing-zero-check
            (callSuccess, newResponses[i]) = target.call(data);

            if (!callSuccess) {
                revert EndpointErrors.OneCallInBatchedCallReverted(lastFrgmtCnt_ + uint8(i));
            }
        }
        return newResponses;
    }

1.Analysis of the Issue

To understand why this error is triggered, it's essential to break down the expected structure of modifiedPacket for the abi.decode function to work correctly. The expected structure for (address, bytes) should be:

1. Address: An Ethereum address, which is 20 bytes long.

2. Bytes: A dynamically sized bytes type that includes a 32-byte length prefix followed by the actual data.

Expected Structure

For abi.decode to function correctly, modifiedPacket should be structured as follows:

- The first 20 bytes represent the address.

- The next 32 bytes represent the length of the bytes array.

- The remaining bytes represent the actual data.

Error Triggering Example

Given the modifiedPacket value of 0x0001000000002000188101000000200018, which is only 20 bytes long, the structure appears as follows: 0x0001000000002000188101000000200018

This 20-byte modifiedPacket is insufficient for abi.decode to extract both an address and a bytes array. Consequently, the decoding operation fails, triggering an EVM error revert.

2.Root Cause

The root cause is that modifiedPacket contains only 20 bytes, which does not meet the requirements for decoding into (address, bytes). Furthermore, even if abi.decode could decode the bytes into data, the target address would be incorrect. The value 0x0001000000002000188101000000200018 represents instructions from packets_[0], not a valid Ethereum address.

Conclusion

The critical issue lies in the incorrect structure of modifiedPacket, which does not conform to the expected (address, bytes) format. This mismatch leads to an EVM error during the abi.decode operation. Moreover, the modifiedPacket is the first packets_[0] corresponding to the instructions instead of the target address and bytes data. Finally, it was identified the inability to execute incoming packets with instructions (from Concrete Backend), incremented the severity of this issue. Furthermore, this failure was attributed to errors during MultiResponse emission, which resulted in the unsuccessful execution of an external call due to improper packet handling.

Proof of Concept

The following Foundry test was used in order to prove the aforementioned issue:

function test_cccmMultiReceiveWithInstructions() public {
        bytes[] memory packets = new bytes[](4);
        packets[0] = abi.encodePacked(
            bytes1(0x00), // 00000000 => flag = 0 and length = 0
            bytes1(0x01), // 00000001 => flag = 0 and length = 1
            hex"00", // index of the first response
            hex"0000", // start at the beginning of the response
            hex"0020", // length of the cropped response (= 32 bytes)
            hex"0018", // byte where the cropped response data should be inserted (24 = 20 + 4 = 0x18)
            bytes1(0x81), // 10000001 => flag = 1 and length = 1
            hex"01", // index of the first response
            hex"0000", // start at the beginning of the response
            hex"0020", // length of the cropped response
            hex"0018" // byte where the cropped response data should be inserted
        );

        // bytes responses[] = new bytes[](3);
        // read the instruction for the first packet. There are none. But the flag is 0, so this is NOT the last packet in the bundle.
        // (bool success, bytes responses[0]) = address(packets[1][0:20]).call(packets[1][20:]])
        // read the instruction for the second packet. There is one. The flag is again 0, so it is NOT the last packet in the bundle. The instructions says: take the response from the first packet (i.e. index 0 = hex"00"), then crop it from the 0th byte (0x00 = 0) and take 32 bytes (0x20 = 32), in other words that would be the entire response (i.e. uint256(B) = 33).
        // and paste it into the calldata. I.e. into packets[2], like so:
        // modifiedPacket = packets[2][0:24] + responses[0][0:32] + packets[2][24 + 32:]  (actually the last part is empty, because the packet[2] only has 56 bytes = address + selector + uint256)
        // NOTE that the original data that was in the call data is now lost. So the number 6 is lost.
        // (bool success, bytes responses[1]) = address(modifiedPacket[0:20]).call(modifiedPacket[20:]])
        // read the instruction for the third packet. There is one. The flag is 1, so it is the lat packet in the bundle.
        // The instructions says: take the response from the first packet (i.e. index 1 = hex"01"), then crop it from byte 0 (0x00 = 0) and take 32 bytes (0x20 = 32), in other words that would be the entire responses[1] (i.e. uint256(A + B) = 45). And paste it into the calldata. I.e. into packets[3], like so:
        // modifiedPacket = packets[3][0:24] + responses[1][0:32] + packets[3][24 + 32:]  (actually the last part is empty, because the packet[3] only has 56 bytes = address + selector + uint256)
        // NOTE that the original data that was in the call data is now lost. So the number 0 is lost.
        // (bool success, bytes responses[2]) = address(modifiedPacket[0:20]).call(modifiedPacket[20:]])
        packets[1] = abi.encodePacked(address(mockCallContract), MockCallContract.getB.selector);  // call number = 0  => response index = 0
        packets[2] = abi.encodeWithSelector(MockCallContract.addToA.selector, 6);
        packets[3] = abi.encodeWithSelector(MockCallContract.addToA.selector, 0);console.log("PACKET0: ",address(bytes20(packets[0])));console.log("PACKET1: ",address(bytes20(packets[1])));console.log("PACKET2: ",address(bytes20(packets[2])));console.log("PACKET3: ",address(bytes20(packets[3])));
        // expected responses
        bytes[] memory responses = new bytes[](3);
        responses[0] = abi.encode(B);
        responses[1] = abi.encode(A + B);
        responses[2] = abi.encode(2 * (A + B));
        // no response template
        bytes memory responseTemplate = "";
        // send the packets without instructions and expect event to be emitted
        vm.startPrank(executor);
        uint224 nonce = uint224(multiMessaging.getNonce());
        bytes32 packetId = bytes32((uint256(nonce) << 32) | uint256(targetEid));
        // check if the event was emitted
        //vm.expectEmit(true, false, false, false, address(multiMessaging));
        //emit EndpointMultiMessageEvents.MultiResponse(packetId, keccak256(abi.encode(packets)), 3, responses);
        multiMessaging.cccmMultiReceive(packetId, eid, packets, responseTemplate);
        vm.stopPrank();
    }

Evidence (EVM: Error revert):

BVSS
Recommendation

Consider solving the structure of modifiedPacket and ensuring it contains valid target addresses and data is crucial to resolving this issue.

Remediation

SOLVED : The Concrete team solved the issue by extracting the calldata from a packet and calls the target address with it properly.

Remediation Hash
f04b22d33f51e6ee93a79cf514be101d0b599736

7.2 Incorrect Fragment Count Leading to External Call Error

// Critical

Description

It has been observed that the extractSubset function is using an incorrect temp.lastFrgmtCnt value, set as 0x0001000000002000188101000000200018.

// insert the instructions and make a low level call.
            (bool callSuccess, bytes memory newResponsesEncoded) = address(this).call(
                abi.encodeWithSelector(
                    this.multipleReceivePseudoInternal.selector,
                    numFrgmt,
                    temp.lastFrgmtCnt, // last fragment from the previous bundle
                    packets_[0].sliceRevertOnInvalid(temp.startByte, endByte), // relevant part of instructions
                    CccmLib.extractSubset(packets_, temp.lastFrgmtCnt, numFrgmt),
                    responses,
                    successFlags
                )
            );

Here, temp.lastFrgmtCnt is incorrectly selecting packets_[0], which corresponds to the instructions. As a result, modifiedPacket is set incorrectly to the hexadecimal values of the instructions, rather than targeting the appropriate packet for performing the external call on the multipleReceivePseudoInternal function.

Proof of Concept

The following Foundry test was used in order to prove the aforementioned issue:

function test_cccmMultiReceiveWithInstructions() public {
        mockCallContract.createSecondMockCallContract();
        MockCallContractSimple secondMockCallContract = MockCallContractSimple(mockCallContract.getSecondMockAddress());
        bytes[] memory packets = new bytes[](5);
        packets[0] = abi.encodePacked(
            bytes1(0x00), // 00000000 => flag = 0 and length = 0. First packet.
            bytes1(0x81), // 00000001 => flag = 0 and length = 1. Second packet. Takes de response from first packet.
            hex"00", // index of the first response
            hex"0000", // start at the beginning of the response
            hex"0020", // length of the cropped response (= 32 bytes)
            hex"0018", // byte where the cropped response data should be inserted (24 = 20 + 4 = 0x18)
            bytes1(0x81), // 10000001 => flag = 1 and length = 1. 
            hex"01", // index of the second response
            hex"0000", // start at the beginning of the response
            hex"0020", // length of the cropped response
            hex"0018", // byte where the cropped response data should be inserted
            bytes1(0x82), // 10000010 => flag = 0 and length = 2
            hex"02", // index of the third response (i.e. the address of the mock contract)
            hex"000c", // start at the beginning of the response
            hex"0014", // length of the cropped response (length of an address, i.e. "0x14"=20 bytes)
            hex"0000", // byte where the cropped response data should be inserted
            hex"01", // index of the second response
            hex"0000", // start at the beginning of the response
            hex"0020", // length of the cropped response
            hex"0018" // byte where the cropped response data should be inserted
        );

        // bytes responses[] = new bytes[](4);
        // read the instruction for the first packet. There are none. But the flag is 0, so this is NOT the last packet in the bundle.
        // (bool success, bytes responses[0]) = address(packets[1][0:20]).call(packets[1][20:]])
        // read the instruction for the second packet. There is one. The flag is again 1, so it is INDEED the last packet in the bundle (first bundle). The instructions says: take the response from the first packet (i.e. index 0 = hex"00"), then crop it from the 0th byte (0x00 = 0) and take 32 bytes (0x20 = 32), in other words that would be the entire response (i.e. uint256(B) = 33).
        // and paste it into the calldata. I.e. into packets[2], like so:
        // modifiedPacket = packets[2][0:24] + responses[0][0:32] + packets[2][24 + 32:]  (actually the last part is empty, because the packet[2] only has 56 bytes = address + selector + uint256)
        // NOTE that the original data that was in the call data is now lost. So the number 6 is lost.
        // (bool success, bytes responses[1]) = address(modifiedPacket[0:20]).call(modifiedPacket[20:]])
        // read the instruction for the third packet. There is one. The flag is 0, so it is NOT the last packet in the bundle.
        // The instructions says: take the response from the first packet (i.e. index 1 = hex"01"), then crop it from byte 0 (0x00 = 0) and take 32 bytes (0x20 = 32), in other words that would be the entire responses[1] (i.e. uint256(A + B) = 45). And paste it into the calldata. I.e. into packets[3], like so:
        // modifiedPacket = packets[3][0:24] + responses[1][0:32] + packets[3][24 + 32:]  (actually the last part is empty, because the packet[3] only has 56 bytes = address + selector + uint256)
        // NOTE that the original data that was in the call data is now lost. So the number 0 is lost.
        // (bool success, bytes responses[2]) = address(modifiedPacket[0:20]).call(modifiedPacket[20:]])
        // read the instruction for the fourth packet. There are two. The flag is 1, so it is INDEED the last packet in the bundle. The second bundle.
        // The first instruction says take the response from the third packet (i.e. index 2 = hex"02"), then crop it from byte 0 (0x00 = 0) and take 20 bytes (0x14 = 20), in other words that would be the entire address of the second mock contract. And paste it into the calldata. I.e. into packets[4], like so:
        // modifiedPacket = responses[2][0:20] + packets[4][20:]
        // Then take the response from the second packet (i.e. index 1 = hex"01"), then crop it from byte 0 (0x00 = 0) and take 32 bytes (0x20 = 32), in other words that would be the entire responses[1] (i.e. uint256(A + B) = 45). And paste it into the calldata. I.e. into packets[4], like so:
        // modifiedPacket = modifiedPacket[0:20] + responses[1][0:32] + packets[4][20 + 32:]  (actually the last part is empty, because the packet[4] only has 52 bytes = address + selector + uint256)
        packets[1] = abi.encodePacked(address(mockCallContract), MockCallContract.getB.selector); // call number = 0  => response index = 0
        packets[2] = abi.encodePacked(address(mockCallContract), MockCallContract.addToA.selector, uint256(6));
        packets[3] = abi.encodePacked(address(mockCallContract), MockCallContract.getAddress.selector, uint256(0));
        packets[4] = abi.encodePacked(address(mockCallContract), MockCallContract.addToA.selector, uint256(6));
        // expected responses
        bytes[] memory responses = new bytes[](4);
        responses[0] = abi.encode(B);
        responses[1] = abi.encode(A + B);
        responses[2] = abi.encode(address(secondMockCallContract));
        responses[3] = abi.encode(A + B);
        // no response template
        bytes memory responseTemplate = "";
        // send the packets without instructions and expect event to be emitted
        vm.startPrank(executor);
        uint224 nonce = uint224(multiMessaging.getNonce());
        bytes32 packetId = bytes32((uint256(nonce) << 32) | uint256(targetEid));
        // create a uint256 from the binary format 001111
        uint256 successFlags = 1 + 2 + 4 + 8;
        // check if the event was emitted
        vm.expectEmit(true, true, true, true, address(multiMessaging));
        emit EndpointMultiMessageEvents.MultiResponse(packetId, keccak256(abi.encode(packets)), successFlags, responses);
        multiMessaging.cccmMultiReceive(packetId, eid, packets, responseTemplate);
        vm.stopPrank();
    }

Evidence:


BVSS
Recommendation

Consider incrementing one to temp.lastFrgmtCnt value in order to use the target address once it decoded instead of instructions.

// insert the instructions and make a low level call.
            (bool callSuccess, bytes memory newResponsesEncoded) = address(this).call(
                abi.encodeWithSelector(
                    this.multipleReceivePseudoInternal.selector,
                    numFrgmt,
                    temp.lastFrgmtCnt, // last fragment from the previous bundle
                    packets_[0].sliceRevertOnInvalid(temp.startByte, endByte), // relevant part of instructions
                    CccmLib.extractSubset(packets_, temp.lastFrgmtCnt +1, numFrgmt),
                    responses,
                    successFlags
                )
            );

Remediation

SOLVED : The Concrete team solved the issue. They incremented to one the temp.lastFrgmtCnt variable in order to select the proper value for executing the external call.

Remediation Hash
f04b22d33f51e6ee93a79cf514be101d0b599736

7.3 Lack of Packet Size Limitation

// Low

Description

It has been identified that there is no restriction on the size of the packets being formed, allowing for the encoding of excessively large packets. This lack of limitation can lead to unexpected behaviors and potential Denial of Service (DoS) attacks when attempting to iterate over packets_.length before making external calls.

function multipleReceiveOneBatchPseudoInternal(bytes[] calldata packets_)
        external
        OnlyPseudoInternalCalls
        returns (bytes[] memory responses)
    {
        bool callSuccess;
        responses = new bytes[](packets_.length - 1);
        for (uint256 i = 1; i < packets_.length; i++) {
            (callSuccess, responses[i - 1]) = address(bytes20(packets_[i][:20])).call(packets_[i][20:]);
            if (!callSuccess) {
                revert EndpointErrors.OneCallInBatchedCallReverted(uint8(i - 1));
            }
        }
    }
Potential Impact:
  • Unexpected Behaviors: The function may exhibit unanticipated actions due to the lack of packet size control.

  • Denial of Service (DoS): Iterating over large packet arrays can lead to significant delays or failures, impacting the system's reliability and availability.

Proof of Concept

The following Foundry test was used in order to prove the aforementioned issue:

function test_cccmMultiReceiveWithOutInstructions_TooMany() public {
        uint256 C = 25;
        // generate two calldata packets. One is for retrieving the B value an
        // the second is for addToA some value
        bytes[] memory packets = new bytes[](10000);
        // ["", packet1, packet2]
        packets[0] = "";
        packets[1] = abi.encodePacked(address(mockCallContract), MockCallContract.getB.selector);
        packets[2] = abi.encodePacked(address(mockCallContract), MockCallContract.addToA.selector, C);

        for (uint256 index = 3; index < 10000; index++) {
            packets[index] = abi.encodePacked(address(mockCallContract), MockCallContract.addToA.selector, C);
        }
        // expected responses
        bytes[] memory responses = new bytes[](2);
        responses[0] = abi.encode(B);
        responses[1] = abi.encode(A + C);
        // no response template
        bytes memory responseTemplate = "";
        // send the packets without instructions and expect event to be emitted
        vm.startPrank(executor);
        uint224 nonce = uint224(multiMessaging.getNonce());
        bytes32 packetId = bytes32((uint256(nonce) << 32) | uint256(targetEid));
        // check if the event was emitted
        vm.expectEmit(true, true, true, true, address(multiMessaging));
        emit EndpointMultiMessageEvents.MultiResponse(packetId, keccak256(abi.encode(packets)), 3, responses);
        multiMessaging.cccmMultiReceive(packetId, eid, packets, responseTemplate);
        vm.stopPrank();
    }

Evidence: Allowed executed a packet of 10000 size, and the huge event emitting with all responses array.

BVSS
Recommendation

Consider implementing a restriction on the maximum allowable size of packets to prevent encoding excessively large packets. Moreover, validate packet sizes before processing to ensure they fall within acceptable limits.

Remediation

SOLVED: The suggested mitigation was applied to the following functions:

  • MultiMessaging::cccmMultiSend()

  • MultiMessaging::cccmMultiReceive()

  • SingleMessaging::cccmSingleSend()

  • SingleMessaging::cccmSingleReceive()

Remediation Hash
e2882d0fd75a8da2bfbfe860d8b7ea87fb0ef99b

7.4 Missing nonReentrant modifier

// Informational

Description

In the MultiMessaging and SingleMessaging contract, the function "Receive" involves an external call to the target packets.

While the contract's current implementation presents no obvious vulnerability due to the fact that the external call is made by the executor (Role guard), it is advisable to include the nonReentrant modifier for enhanced security and to prevent unexpected behavior. This is a safer practice and can prevent potential issues in future updates or unforeseen scenarios.

BVSS
Recommendation

Add a nonReentrant type modifier to the function to further enhance security.

Remediation

SOLVED: A modified reentrancy guard was added, which prevents smart contracts from reentering.

   modifier nonReentrantForContracts() {
         if (msg.sender == tx.origin) {
             _;
         } else {
             _nonReentrantBefore();
             _;
             _nonReentrantAfter();
         }
     }
Remediation Hash
6533fb5200d9cf409e905786dfbc6efc43e6adf0

7.5 No Events for State Changes

// Informational

Description

Adding events to the setEid and setNativeTokenRecipient functions would provide better traceability of state changes.


Score
Recommendation

Consider defining and emit events for these functions.

event EidSet(uint32 indexed newEid);
event NativeTokenRecipientSet(address indexed newRecipient);

function setEid(uint32 eid_) external override(IEid) onlyRole(EndpointRoles.SETTER) {
    if (eid_ == 0) revert EndpointErrors.InvalidTargetEid(eid_);
    EID = eid_;
    emit EidSet(eid_);
}

function setNativeTokenRecipient(address payable recipient_) external override(ISendNativeToken) onlyRole(EndpointRoles.MANAGER) {
    if (recipient_ == address(0)) revert EndpointErrors.InvalidNativeTokenRecipientAddress();
    _nativeTokenRecipient = recipient_;
    emit NativeTokenRecipientSet(recipient_);
}

Remediation

SOLVED: The suggested mitigation was implemented by the Concrete team.

Remediation Hash
849c5768b24ecba39f5f374225ea3b3f3e78d886

8. Automated Testing

Halborn used automated testing techniques to enhance the coverage of certain areas of the scoped contracts. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified all the contracts in the repository and was able to compile them correctly into their ABI and binary formats, Slither was run on the all-scoped 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. Some High and Medium issues have been found using automated testing that may be it's important to be considered:

Output:

Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.

© Halborn 2024. All rights reserved.