Prepared by:
HALBORN
Last Updated 05/08/2024
Date of Engagement by: April 12th, 2024 - April 19th, 2024
100% of all REPORTED Findings have been addressed
All findings
20
Critical
0
High
0
Medium
0
Low
8
Informational
12
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.
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.
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.
External libraries and financial-related attacks.
New features/implementations after/within the remediation commit IDs.
EXPLOITABILIY METRIC () | METRIC VALUE | NUMERICAL 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 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL 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 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
0
High
0
Medium
0
Low
8
Informational
12
Security analysis | Risk level | Remediation Date |
---|---|---|
Lack of storage verification allows for duplicated public keys | Low | Solved - 05/02/2024 |
Missing threshold verification for fees | Low | Risk Accepted |
Lack of input validation for destination chain id may allow for requests to invalid chains | Low | Risk Accepted |
Response functions can be called during contract's paused state allowing to continue contract operations | Low | Solved - 05/02/2024 |
Lack of match identification between response functions caller and recovered address | Low | Risk Accepted |
Missing traceId verification in signature resolve function | Low | Solved - 05/07/2024 |
Contract may emit multiple resolving events for the same data | Low | Solved - 05/02/2024 |
Insufficient public key validation | Low | Risk Accepted |
Improper contract naming conventions in upgradeable OpenZeppelin inheritance | Informational | Solved - 05/02/2024 |
Missing namespace NatSpec tag for ERC-7201 | Informational | Solved - 05/02/2024 |
PUSH0 is not supported by all chains | Informational | Solved - 05/02/2024 |
Transfer with hardcoded gas amount | Informational | Solved - 05/02/2024 |
Unused `SRC_WALLET_TYPE_ID` constant declaration | Informational | Acknowledged |
Public functions not called within the contract can be made external | Informational | Acknowledged |
Lack of verification for config functions input values | Informational | Acknowledged |
Unnecessary payable modifier in `withdrawFees()` | Informational | Solved - 05/02/2024 |
Typo in function name | Informational | Solved - 05/02/2024 |
Misleading error name | Informational | Solved - 05/02/2024 |
Unnecessary storage access | Informational | Solved - 05/02/2024 |
Missing initialization function for AccessControl | Informational | Solved - 05/02/2024 |
// Low
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
);
}
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();
}
Consider adding a check to verify that the public key does not already exist on the mapping.
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.
// Low
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;
Consider adding checks to the setupBaseFee()
and setupNetworkFee()
functions to verify that the proposed fees fall within a threshold.
RISK ACCEPTED: The ZenRock Labs team made a business decision to accept the risk of this finding and not alter the contracts.
// Low
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
);
}
Consider adding a check to the _sigReq
function to verify that the dstChainId
parameter is a valid chain ID.
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.
// Low
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.
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);
}
Consider adding the whenNotPaused
modifier to the aforementioned functions to prevent them from being executed when the contract is in a paused state.
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.
// Low
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.
Consider adding a check to the _mustValidateAuthSignature()
function to verify that the authAddress
recovered is the msg.sender
of the transaction.
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.
// Low
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.
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.
SOLVED: The ZenRock Labs team has addressed the finding in commit 5ac1dfc
by following the mentioned recommendation.
// Low
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.
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);
}
}
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.
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.
// Low
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.
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);
}
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.
// Informational
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"
It is recommended to not modify OpenZeppelin's original codebase, including naming conventions. Maintain the conventional naming pattern for OpenZeppelin's upgradeable contracts:contractNameUpgradeable
.
SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49
by following the mentioned recommendation.
// Informational
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.
Consider adding the following namespace tag to properly follow the ERC-7201 specification:
/// @custom:storage-location erc7201:zrsign.storage.sign
SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49
by following the mentioned recommendation.
// Informational
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.
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.
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
.
// Informational
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.
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/.
SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49
by following the mentioned recommendation.
// Informational
The SRC_WALLET_TYPE_ID
constant declared in Sign.sol
is not being used throughout the contract. This may indicate missing or unfinished implementations.
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.
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.
// Informational
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.
Modify the aforementioned functions with the external
visibility modifier.
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.
// Informational
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.
Verify that the aforementioned input values provided are non-empty or valid.
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
// Informational
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.
Remove the payable
modifier from the withdrawFees()
function to correctly reflect its intended use and to prevent erroneous transfers to the function.
SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49
by following the mentioned recommendation.
// Informational
The function uppause()
in the ZrSign
contract appears to be misnamed, as its action is to unpause the contract.
Rename uppause()
to unpause()
to accurately reflect its purpose.
SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49
by following the mentioned recommendation.
// Informational
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.
Consider renaming the error to a name that accurately describes the issue.
SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49
by following the mentioned recommendation.
// Informational
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.
Avoid direct storage access for the walletIndex
and use the params.walletIndex
value instead to improve efficiency and reduce gas costs.
SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49
by following the mentioned recommendation.
// Informational
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.
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.
SOLVED: The ZenRock Labs team has addressed the finding in commit 4492c49
by following the mentioned recommendation.
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.
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.
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.
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