Halborn Logo

Zr-sign - Zenrock Laboratories


Prepared by:

Halborn Logo

HALBORN

Last Updated 05/08/2024

Date of Engagement by: April 12th, 2024 - April 19th, 2024

Summary

100% of all REPORTED Findings have been addressed

All findings

20

Critical

0

High

0

Medium

0

Low

8

Informational

12


1. Introduction

ZenRockLabs engaged Halborn to conduct a security assessment on their smart contracts beginning on 04-12-2024 and ending on 04-19-2024. The security assessment was scoped to the smart contracts provided in the https://github.com/zenrocklabs/zr-sign GitHub repository. Commit hashes and further details can be found in the Scope section of this report.

2. Assessment Summary

Halborn was provided 1 week for the 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, which were mostly addressed by the Zenrock Labs team.

The main identified issues were:

    • Response functions can be called during contract's paused state, allowing to continue contract operations.

    • Lack of match identification between response function caller and recovered address.

    • Lack of storage verification allows for duplicated public keys.

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 contract, 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: zr-sign
(b) Assessed Commit ID: 6c40093
(c) Items in scope:
  • contracts/proxy/ZrProxy.sol
  • contracts/zr/Sign.sol
  • contracts/zr/ZrSign.sol
Out-of-Scope: contracts/AccessControl.sol, contracts/Context.sol, contracts/ERC165.sol, contracts/Initializable.sol, contracts/Migrations.sol, contracts/Pausable.sol, contracts/proxy/ERC1967Proxy.sol, contracts/proxy/ERC1967Upgrade.sol, contracts/proxy/Proxy.sol
Remediation Commit ID:
Out-of-Scope: New features/implementations after the remediation commit IDs.

6. Assessment Summary & Findings Overview

Critical

0

High

0

Medium

0

Low

8

Informational

12

Security analysisRisk levelRemediation Date
Lack of storage verification allows for duplicated public keysLowSolved - 05/02/2024
Missing threshold verification for feesLowRisk Accepted
Lack of input validation for destination chain id may allow for requests to invalid chainsLowRisk Accepted
Response functions can be called during contract's paused state allowing to continue contract operationsLowSolved - 05/02/2024
Lack of match identification between response functions caller and recovered addressLowRisk Accepted
Missing traceId verification in signature resolve functionLowSolved - 05/07/2024
Contract may emit multiple resolving events for the same dataLowSolved - 05/02/2024
Insufficient public key validationLowRisk Accepted
Improper contract naming conventions in upgradeable OpenZeppelin inheritanceInformationalSolved - 05/02/2024
Missing namespace NatSpec tag for ERC-7201InformationalSolved - 05/02/2024
PUSH0 is not supported by all chainsInformationalSolved - 05/02/2024
Transfer with hardcoded gas amountInformationalSolved - 05/02/2024
Unused `SRC_WALLET_TYPE_ID` constant declarationInformationalAcknowledged
Public functions not called within the contract can be made externalInformationalAcknowledged
Lack of verification for config functions input valuesInformationalAcknowledged
Unnecessary payable modifier in `withdrawFees()`InformationalSolved - 05/02/2024
Typo in function nameInformationalSolved - 05/02/2024
Misleading error nameInformationalSolved - 05/02/2024
Unnecessary storage accessInformationalSolved - 05/02/2024
Missing initialization function for AccessControlInformationalSolved - 05/02/2024

7. Findings & Tech Details

7.1 Lack of storage verification allows for duplicated public keys

// Low

Description

The _zrKeyRes function is responsible for adding new public keys to the wallets mapping once it confirms the signature is valid and the public key length meets the required threshold. Nevertheless, it fails to verify whether the public key is already present in the string array within the wallets mapping of the SignStorage struct. This would allow for duplicated public keys to be added to the mapping.

  function _zrKeyRes(SignTypes.ZrKeyResParams memory params) internal virtual {
        SignStorage storage $ = _getSignStorage();

        bytes memory payload = abi.encode(
            SRC_CHAIN_ID,
            params.walletTypeId,
            params.owner,
            params.walletIndex,
            params.publicKey
        );

        bytes32 payloadHash = keccak256(payload).toEthSignedMessageHash();
        _mustValidateAuthSignature(payloadHash, params.authSignature);

        _validatePublicKey(params.publicKey);

        bytes32 id = _getId(params.walletTypeId, params.owner);

        if ($.wallets[id].length != params.walletIndex) {
            revert IncorrectWalletIndex({
                expectedIndex: $.wallets[id].length,
                providedIndex: params.walletIndex
            });
        }

        $.wallets[id].push(params.publicKey);
        emit ZrKeyResolve(
            params.walletTypeId,
            params.owner,
            $.wallets[id].length - 1,
            params.publicKey
        );
    }
Proof of Concept

The following code uses the same string value for storing on two different indices of the public key array of the wallets mapping:

  function test_ZrKeyResDuplicatedPublicKey(string memory publicKey) public {
    if (bytes(publicKey).length < 5) publicKey = string.concat(publicKey, "abcde");
    test_WalletTypeIdConfigByOwner();

    (address signer, uint256 signerPrivateKey) = makeAddrAndKey("MPC"); // Get private key to sign locally
    bytes32 dataToSign = _getSignedPayload(_getWalletTypeId(), signer, 0, publicKey); // Encode data to sign
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, dataToSign); // Sign the data
    bytes memory signature = abi.encodePacked(r, s, v); // Get the signature

    vm.startPrank(MPC);
    SignTypes.ZrKeyResParams memory zrKeyResParams = SignTypes.ZrKeyResParams({
      walletTypeId: _getWalletTypeId(),
      owner: MPC,
      walletIndex: 0,
      publicKey: publicKey,
      authSignature: signature
    });

    // Execute first Key Response
    zrSign.zrKeyRes(zrKeyResParams);
    string[] memory keys = zrSign.getZrKeys(_getWalletTypeId(), MPC);
    uint256 keysLength = keys.length;
    assertEq(keysLength, 1);

    dataToSign = _getSignedPayload(_getWalletTypeId(), signer, 1, publicKey); // Encode data to sign
    (v, r, s) = vm.sign(signerPrivateKey, dataToSign); // Sign the data
    signature = abi.encodePacked(r, s, v); // Get the signature
    zrKeyResParams = SignTypes.ZrKeyResParams({
      walletTypeId: _getWalletTypeId(),
      owner: signer,
      walletIndex: 1,
      publicKey: publicKey,
      authSignature: signature
    });

    zrSign.zrKeyRes(zrKeyResParams);
    keys = zrSign.getZrKeys(_getWalletTypeId(), MPC);
    keysLength = keys.length;
    assertEq(keysLength, 2);

    // Assert that the same key can be added twice
    string memory firstKey = zrSign.getZrKey(_getWalletTypeId(), MPC, 0);
    string memory secondKey = zrSign.getZrKey(_getWalletTypeId(), MPC, 1);
    assertEq(firstKey, secondKey);
    vm.stopPrank();
  }
BVSS
Recommendation

Consider adding a check to verify that the public key does not already exist on the mapping.

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation and adding a check to verify that the public key does not already exist on the mapping prior to storing it in contract.

Remediation Hash
References
Sign.sol#L437-L469

7.2 Missing threshold verification for fees

// Low

Description

The ZrSign contract contains the setupBaseFee and setupNetworkFee. These functions allow the owner to set the base and network fee for the contract. These fees are necessary for calculating the value that the caller has to provide to the contract to pay for the transaction, particularly verified inside the keyFee() and sigFee() modifiers.

However, none of these functions verify that the proposed fees fall within a threshold to avoid potential issues, i.e. integer overflow, when performing operations on these values inside the sigFee() modifier:

uint256 networkFee = payload.length * $._networkFee;
uint256 totalFee = $._baseFee + networkFee;
BVSS
Recommendation

Consider adding checks to the setupBaseFee() and setupNetworkFee() functions to verify that the proposed fees fall within a threshold.

Remediation Plan

RISK ACCEPTED: The ZenRock Labs team made a business decision to accept the risk of this finding and not alter the contracts.

References
ZrSign.sol#L90-L94
ZrSign.sol#L102-L106

7.3 Lack of input validation for destination chain id may allow for requests to invalid chains

// Low

Description

The _sigReq function, utilized by external functions zrSignHash, zrSignData, and zrSignTx, handles hashing and signing data operations. This function is accessible to any user via the mentioned external functions. However, it does not validate the dstChainId parameter. As a result, it permits requests with arbitrary and possibly invalid destination chain IDs, which could lead to incorrect increment of the traceID in the contract's storage for invalid requests.

  function _sigReq(SignTypes.SigReqParams memory params) internal virtual sigFee(params.payload) whenNotPaused {
    SignStorage storage $ = _getSignStorage();
    _validatePublicKey(_getWalletByIndex(params.walletTypeId, params.owner, params.walletIndex));
    bytes32 walletId = keccak256(abi.encode(params.walletTypeId, params.owner, params.walletIndex));
    unchecked {
      $._traceId = $._traceId + 1;
    }
    emit ZrSigRequest(
      $._traceId,
      walletId,
      params.walletTypeId,
      params.owner,
      params.walletIndex,
      params.dstChainId,
      params.payload,
      params.isHashDataTx,
      params.broadcast
    );
  }
BVSS
Recommendation

Consider adding a check to the _sigReq function to verify that the dstChainId parameter is a valid chain ID.

Remediation Plan

RISK ACCEPTED: The ZenRock Labs team made a business decision to accept the risk of this finding and not alter the contracts, asserting that: The function in question is accessible via an external function that has a modifier to perform the necessary checks, ensuring controlled and safe usage.

References
Sign.sol#L483-L511

7.4 Response functions can be called during contract's paused state allowing to continue contract operations

// Low

Description

The _zrKeyRes() function, triggered by the external zrKeyRes() function, is responsible for processing key response requests. This process includes validating the authenticity and integrity of the response through signature verification and updating the contract's state, specifically the wallets mapping. However, this function lacks a check to prevent execution when the contract is in a paused state, which could allow ongoing operations that modify the contract's state.

Similarly, the _sigRes() function, triggered by the external zrSigRes() function, is responsible for finalizing the signature response by emitting an event. This function also lacks a check to prevent execution when the contract is in a paused state, which could allow ongoing operations.

Proof of Concept

Here, the ZrKeyRes function is called when the contract is paused, succesfully updating contract's public keys in storage:

  function test_ResponseFunctionsWhenContractIsPaused(string memory publicKey) public {
    test_pause();
    assertTrue(zrSign.paused());
    string[] memory keys = zrSign.getZrKeys(_getWalletTypeId(), MPC);
    uint256 keysLength = keys.length;
    assertEq(keysLength, 0);
    test_ZrKeyRes(publicKey);
    keys = zrSign.getZrKeys(_getWalletTypeId(), MPC);
    keysLength = keys.length;
    assertEq(keysLength, 1);
    assertTrue(zrSign.paused());
  }
}

 function test_ZrKeyRes(string memory publicKey) public {
   if (bytes(publicKey).length < 5) publicKey = string.concat(publicKey, "abcde");
   test_WalletTypeIdConfigByOwner(); // make the wallet type Id supported (44-60)
    
   (address signer, uint256 signerPrivateKey) = makeAddrAndKey("MPC"); // Get private key to sign locally
   bytes32 dataToSign = _getSignedPayload(_getWalletTypeId(), signer, 0, publicKey); // Encode data to sign
   (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, dataToSign); // Sign the data
   bytes memory signature = abi.encodePacked(r, s, v); // Get the signature
    
   vm.startPrank(MPC);
   SignTypes.ZrKeyResParams memory zrKeyResParams = SignTypes.ZrKeyResParams({
     walletTypeId: _getWalletTypeId(),
     owner: MPC,
     walletIndex: 0,
     publicKey: publicKey,
     authSignature: signature
   });
    
   zrSign.zrKeyRes(zrKeyResParams);
   string[] memory keys = zrSign.getZrKeys(_getWalletTypeId(), MPC);
   uint256 keysLength = keys.length;
   assertEq(keysLength, 1);
 }
BVSS
Recommendation

Consider adding the whenNotPaused modifier to the aforementioned functions to prevent them from being executed when the contract is in a paused state.

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation and adding the whenNotPaused modifier to the _zrKeyRes() and the _sigRes functions.

Remediation Hash
References
Sign.sol#L145-L149
Sign.sol#L277-L279

7.5 Lack of match identification between response functions caller and recovered address

// Low

Description

The zrKeyRes() and zrSignRes() functions, which can be called by any address, utilize the _mustValidateAuthSignature() function for validating data signatures. Currently, _mustValidateAuthSignature() does not verify whether the authAddress recovered from the signature is the same as the msg.sender of the transaction. This lack of verification could potentially allow an unauthorized user to execute these functions and pass validation checks on behalf of another address.

BVSS
Recommendation

Consider adding a check to the _mustValidateAuthSignature() function to verify that the authAddress recovered is the msg.sender of the transaction.

Remediation Plan

RISK ACCEPTED: The ZenRock Labs team made a business decision to accept the risk of this finding and not alter the contracts, asserting that: This behavior is intentional, allowing anyone with the correct signature and data to pay for L1 gas fees.

References
Sign.sol#L553-L566

7.6 Missing traceId verification in signature resolve function

// Low

Description

The _sigRes() function, accessible via the external zrSignRes() function, is tasked with finalizing signature responses and emitting an event. However, it does not validate the traceId to confirm that it is neither previously resolved nor nonexistent. This lack of validation can lead to the same traceId being emitted multiple times or the use of invalid traceIds, potentially compromising data integrity.

BVSS
Recommendation

Consider introducing a validation mechanism to ensure that the traceId is legitimate. This will prevent duplicate or invalid traceIds from compromising the integrity of the transaction records.

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 5ac1dfc by following the mentioned recommendation.

Remediation Hash
References
Sign.sol#L525-L537

7.7 Contract may emit multiple resolving events for the same data

// Low

Description

The _sigRes() function, accessible via the external zrSignRes() function, is responsible for finalizing the signature response does not have a verification mechanism to verify that the function input data has been executed previously and emitted as resolved.

This oversight leads to redundant events and opens the door for potential spamming, as malicious actors could exploit this to flood the network with repetitive transactions.

Proof of Concept

Here, the same data is emitted in different transactions:

  function test_ZrSignResRepeatedData(
    string memory chainId,
    string memory publicKey,
    uint256 _newBaseFee,
    uint256 _newNetworkFee
  ) public {
    test_ZrSignTx(chainId, publicKey, _newBaseFee, _newNetworkFee);
    (address signer, uint256 signerPrivateKey) = makeAddrAndKey("MPC"); // Get private key to sign locally
    bytes32 dataToSign = _getSignedPayload(_getWalletTypeId(), signer, 0, publicKey); // Encode data to sign
    (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, dataToSign); // Sign the data
    bytes memory signature = abi.encodePacked(r, s, v); // Get the signature

    bytes memory payload = abi.encode(SRC_CHAIN_ID, 2, signature, true);
    bytes32 newDataToSign = keccak256(payload).toEthSignedMessageHash();
    (v, r, s) = vm.sign(signerPrivateKey, newDataToSign); // Sign the data
    bytes memory newSignature = abi.encodePacked(r, s, v); // Get the signature

    SignTypes.SignResParams memory signResParams = SignTypes.SignResParams({
      traceId: 2,
      signature: signature,
      broadcast: true,
      authSignature: newSignature
    });

    for (uint i; i < 10; ++i) {
      vm.expectEmit(address(zrSign));
      emit ZrSigResolve(2, signature, true);
      zrSign.zrSignRes(signResParams);
    }
  }
BVSS
Recommendation

Consider adding a check to the zrSignRes function or the _sigRes() function to not allow for data that has been executed previously and emitted as resolved.

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation and adding a check to verify that the data has not been previously processed.

Remediation Hash
References
Sign.sol#L525-L537

7.8 Insufficient public key validation

// Low

Description

The Sign.sol contract contains the _validatePublicKey function which validates a public key by checking for a minimum key length of 4 bytes. This threshold is substantially below the length required for secure public keys on blockchain technologies like Bitcoin and Ethereum. In Bitcoin, for example, compressed public keys are 33 bytes, and uncompressed keys are 65 bytes. The inadequacy of the check may permit the use of invalid or insecure public keys, undermining the security of the contract and potentially exposing it to unexpected risks.

BVSS
Recommendation

Enforce stricter validation that aligns with accepted cryptographic standards. Specifically, is it recommended updating the _validatePublicKey function by requiring that the minimum length is greater than 32 bytes.

For Ethereum addresses, which are derived from the rightmost 160 bits of the Keccak-256 hash of the public key, an additional check can ensure the address falls within the appropriate range:

bytes memory pubKeyBytes = bytes(publicKey);

if (uint256(bytes32(pubKeyBytes)) > type(uint160).max) {
  revert InvalidEthereumAddress(ownerBytes);
}

Remediation Plan

RISK ACCEPTED: The ZenRock Labs team made a business decision to accept the risk of this finding and not alter the contracts, asserting that: Our contract is designed to accommodate a wide range of wallet types, including BTC addresses, EVM addresses, and Cosmos wallets.

References
Sign.sol#L721-L726

7.9 Improper contract naming conventions in upgradeable OpenZeppelin inheritance

// Informational

Description

The Sign.sol contract inherits from a local version of OpenZeppelin upgradeable contracts. These contracts contain code that pertains to the Upgradeable version of themselves, signaling a name modification removing the Upgradeable suffix added to upgradeable contracts.

The affected contracts are:

- AccessControl in "../contracts/AccessControl.sol"

- Context in "../contracts/Context.sol"

- Pausable in "../contracts/Pausable.sol"

Score
Recommendation

It is recommended to not modify OpenZeppelin's original codebase, including naming conventions. Maintain the conventional naming pattern for OpenZeppelin's upgradeable contracts:contractNameUpgradeable.

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation.

Remediation Hash
References
Sign.sol#L17

7.10 Missing namespace NatSpec tag for ERC-7201

// Informational

Description

Currently, the Sign contract does not follow the NatSpec tag specification that should be annotated to the contract's struct for ERC-7201. According to ERC-7201:

A namespace in a contract should be implemented as a struct type. These structs should be annotated with the NatSpec tag @custom:storage-location <FORMULA_ID>:<NAMESPACE_ID>, where <FORMULA_ID> identifies a formula used to compute the storage location where the namespace is rooted, based on the namespace id.


Score
Recommendation

Consider adding the following namespace tag to properly follow the ERC-7201 specification:

/// @custom:storage-location erc7201:zrsign.storage.sign

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation.

Remediation Hash

7.11 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

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation, and locking the contracts to compiler version 0.8.19.

Remediation Hash
References
ZrProxy.sol#L6
Sign.sol#L4
ZrSign.sol#L4

7.12 Transfer with hardcoded gas amount

// Informational

Description

The ZrSign contract contains the withdrawFees. This function allows an address with the TOKENOMICS_ROLE to withdraw all funds from the contract. It does so by calling the transfer function.

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: _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 https://swcregistry.io/docs/SWC-134/.

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation.

Remediation Hash
References
ZrSign.sol#L115

7.13 Unused `SRC_WALLET_TYPE_ID` constant declaration

// Informational

Description

The SRC_WALLET_TYPE_ID constant declared in Sign.sol is not being used throughout the contract. This may indicate missing or unfinished implementations.

Score
Recommendation

Assess if the constant was intended for features that have not yet been implemented. If so, complete these implementations to utilize the constant effectively. If the SRC_WALLET_TYPE_ID constant is not planned for use in present or upcoming features, consider removing it.

Remediation Plan

ACKNOWLEDGED: The ZenRock Labs team made a business decision to acknowledge this finding and not alter the contracts, asserting: We recognize this and choose to retain the constant for future utility.

References
Sign.sol#L26-L27

7.14 Public functions not called within the contract can be made external

// Informational

Description

The Sign.sol contract currently defines the getTraceId() and isWalletTypeSupported() 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

ACKNOWLEDGED: The ZenRock Labs team made a business decision to acknowledge this finding and not alter the contracts, asserting: We acknowledge this issue. Although these functions are currently not utilized, we intend to keep them available for potential future use.

References
Sign.sol#L289
Sign.sol#L384

7.15 Lack of verification for config functions input values

// Informational

Description

The ZrSign contract’s chainIdConfig function allows the ADMIN to set the chain ID for specific wallet types, similar to walletTypeIdConfig which configures support for specific wallet types based on their purpose and coin type.

However, both functions lack validation for their inputs. This oversight could permit the assignment of invalid or unexpected values, such as an empty string for the chainId or 0 values for the walletTypeId, purpose or coinType input values, potentially compromising the contract's informational integrity.

Score
Recommendation

Verify that the aforementioned input values provided are non-empty or valid.

Remediation Plan

ACKNOWLEDGED: The ZenRock Labs team made a business decision to acknowledge this finding and not alter the contracts, asserting: We acknowledge this issue and are considering appropriate modifications to enhance verification processes without compromising functionality

References
ZrSign.sol#L56-L64
ZrSign.sol#L74-L82

7.16 Unnecessary payable modifier in `withdrawFees()`

// Informational

Description

In the current implementation of the withdrawFees() function within the ZrSign contract, the use of the payable modifier is unnecessary and potentially misleading. The payable modifier in Solidity is used to allow a function to receive native assets along with a call. However, withdrawFees() is designed to send native assets (the collected fees) from the contract to the function caller, and thus should not accept incoming assets transactions.

Score
Recommendation

Remove the payable modifier from the withdrawFees() function to correctly reflect its intended use and to prevent erroneous transfers to the function.

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation.

Remediation Hash
References
ZrSign.sol#L112-L117

7.17 Typo in function name

// Informational

Description

The function uppause() in the ZrSign contract appears to be misnamed, as its action is to unpause the contract.

Score
Recommendation

Rename uppause() to unpause() to accurately reflect its purpose.

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation.

Remediation Hash
References
ZrSign.sol#L123-L125

7.18 Misleading error name

// Informational

Description

The _mustValidateAuthSignature function reverts with the UnauthorizedCaller error if the authAddress recovered from the data and signature provided does not have the MPC_ROLE. However, the error name is misleading, as it assumes that the caller of the transaction is the authAddress recovered, which may not always be the case.

Score
Recommendation

Consider renaming the error to a name that accurately describes the issue.

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation.

Remediation Hash
References
Sign.sol#L564

7.19 Unnecessary storage access

// Informational

Description

The _zrKeyRes() function emits the ZrKeyResolve event and calculates the walletIndex by unnecessarily reading from storage: wallets[id].length - 1. This approach is inefficient as it incurs higher gas costs.

Score
Recommendation

Avoid direct storage access for the walletIndex and use the params.walletIndex value instead to improve efficiency and reduce gas costs.

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation.

Remediation Hash
References
Sign.sol#L466

7.20 Missing initialization function for AccessControl

// Informational

Description

The Sign contract employs a transparent upgradeable pattern and initiates the Pausable_init_unchained() and Sign_init_unchained() within the _Sign_init() function during the contract's initialization. However, it fails to call the _AccessControl_init() or the _AccessControl_init_unchained() function. While this oversight does not impact the contract's functionality, owing to the currently empty implementation of this initialization function, it diverges from best practices associated with using the AccessControlUpgradeable module.

Score
Recommendation

Add the _AccessControl_init_unchained() function call to the __Sign_init() function to align with standard practices for initializing inherited AccessControl mechanisms in the Sign contract.

Remediation Plan

SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49 by following the mentioned recommendation.

Remediation Hash
References
Sign.sol#L120-L123

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 not included in the report because they were determined as false positives.

ZenRockLabs - Slither 1ZenRockLabs - Slither 2

Unit tests and fuzzing

The original repository used the Truffle 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.


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.

© Halborn 2024. All rights reserved.