Prepared by:
HALBORN
Last Updated 04/25/2024
Date of Engagement by: July 12th, 2023 - July 21st, 2023
100% of all REPORTED Findings have been addressed
All findings
13
Critical
0
High
0
Medium
0
Low
3
Informational
10
Saferoot
is designed to provide additional safety features for the users of ERC-20, ERC-721, and ERC-1155 tokens. It provides a safety mechanism in case of a mistake, hack, or scam by transferring the users' tokens from their wallet to their backup address before the transaction that triggered the safeguard executes.
\client engaged Halborn
to conduct a security assessment on their smart contracts beginning on 2023-07-12 and ending on 2023-07-21. The security assessment was scoped to the smart contracts provided in the Staging-Labs/Saferoot-Contract GitHub repository. Commit hashes and further details can be found in the Scope section of this report.
Halborn was provided one week for the engagement and assigned a team of one full-time security engineer to review the security of the smart contracts in scope. The security team consists of a blockchain and smart contract security experts 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 that were mostly addressed by Staging Labs.
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
Scanning of solidity files for vulnerabilities, security hot-spots or bugs. (MythX
)
Static Analysis of security for scoped contract, and imported functions. (Slither
)
Testnet deployment (Foundry
, Brownie
)
Saferoot Contracts
Repository: Staging-Labs/Saferoot-Contract
Commit ID: 834455599dacda269ed222182c3576bf37d70038
Smart contracts in scope:
contracts/ContractRegistry.sol
contracts/ErrorReporter.sol
contracts/Saferoot.sol
contracts/SaferootFactory.sol
Final fix commit ID: 0884050
contracts/ContractRegistry.sol
contracts/ErrorReporter.sol
contracts/Saferoot.sol
contracts/SaferootFactory.sol
Out-of-scope:
third-party libraries and dependencies
economic attacks
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
3
Informational
10
Security analysis | Risk level | Remediation Date |
---|---|---|
MISSING ZERO ADDRESS CHECKS | Low | Solved - 08/07/2023 |
REGISTRY CANNOT BE RE-ENABLED | Low | Solved - 08/02/2023 |
REGISTRY CANNOT BE CHANGED | Low | Solved - 08/02/2023 |
LACK OF SAFEGUARD DATA VALIDATION | Informational | Solved - 08/08/2023 |
LACK OF DISABLEINITIALIZERS IN THE IMPLEMENTATION CONTRACT | Informational | Solved - 08/04/2023 |
ARBITRARY AMOUNT OF SAFEGUARDS CAN BE ADDED | Informational | Acknowledged - 08/23/2023 |
INCOMPATIBILITY WITH TOKENS NOT FOLLOWING THE STANDARDS | Informational | Acknowledged - 08/23/2023 |
MULTIPLE SAFEGUARDS CAN BE ADDED TO THE SAME ASSET | Informational | Solved - 08/23/2023 |
ITERATING OVER A DYNAMIC ARRAY | Informational | Acknowledged - 08/18/2023 |
MISTAKENLY SENT TOKENS AND ETHER CANNOT BE RECOVERED FROM THE CONTRACTS | Informational | Solved - 08/08/2023 |
FOR LOOPS CAN BE GAS OPTIMIZED | Informational | Solved - 08/08/2023 |
UNNECESSARY VALIDATION | Informational | Solved - 08/07/2023 |
NOT ALL EVM COMPATIBLE CHAIN SUPPORTS SOLIDITY 0.8.20 | Informational | Acknowledged - 08/18/2023 |
// Low
The backup
state variable is used to store the address where the user's tokens are transferred if safeguards are initiated. However, it was identified that this variable lacks zero address validation when configured. Setting an invalid backup
address results in loss of funds.
The service
state variable is used to store the address of the account that is authorized to initiate safeguards. This variable also lacks zero address validation. Configuring an invalid address prevents the Saferoot
contract from initiating safeguards.
The contractRegistry
state variable points to the contract's registry used for token verification. If it is set to zero when the registry is enabled, no safeguards can be added.
The user
state variable points to the contract's user address. If set incorrectly, the contract's functions are inaccessible.
function initialize(
address _service,
address _user,
address _backup,
address _contractRegistry,
bool _registryEnabled
) public initializer {
user = _user;
backup = _backup;
service = _service;
contractRegistry = _contractRegistry;
registryEnabled = _registryEnabled;
_grantRole(SERVICE_ROLE, _service);
_grantRole(USER_ROLE, _user);
emit SaferootCreated(_user, _service);
}
function setBackupWallet(address _backup) external onlyUser {
backup = _backup;
emit BackupUpdated(_backup);
}
SOLVED: The \client team solved the issue in commit 9e26e35 by adding zero address checks.
// Low
The ContractRegistry
contract is responsible for storing the addresses of the token contracts that are Saferoot
verified. If this contract is disabled in the Saferoot
contract, it can no longer be re-enabled. As a result, users could no longer use it to verify the token contracts when adding safeguards.
If the registry is enabled in the Saferoot
contract, it is used to verify the token contracts when adding safeguards:
if (
registryEnabled &&
!registry.isContractSupported(incomingSafeguard.contractAddress)
) {
revert InvalidContractAddress();
}
However, there is no enableRegistry
function implemented in the Saferoot
contract. The user can only disable the registry:
function disableRegistry() external onlyUser {
registryEnabled = false;
emit RegistryFlagUpdated(false);
}
SOLVED: The \client team concluded that the contract registry was unnecessary and removed it from the protocol in commit 5166c1d.
// Low
The ContractRegistry
contract is responsible for storing the addresses of the contracts that are Saferoot
verified. Users may wish to use a different implementation, but once the Saferoot
contract is initialized, it is no longer possible to update the registry address in it.
function initialize(
address _service,
address _user,
address _backup,
address _contractRegistry,
bool _registryEnabled
) public initializer {
user = _user;
backup = _backup;
service = _service;
contractRegistry = _contractRegistry;
registryEnabled = _registryEnabled;
_grantRole(SERVICE_ROLE, _service);
_grantRole(USER_ROLE, _user);
emit SaferootCreated(_user, _service);
}
SOLVED: The \client team concluded that the contract registry was unnecessary and removed it from the protocol in commit 5166c1d.
// Informational
The safeguards are used to transfer the tokens from the user to the backup wallet address if they are initiated. It was identified that users can create safeguards with parameters that are not related to their token type. For example, it is possible to create an ERC20 token safeguard with a non-zero tokenId
parameter. Using an invalid parameter indicates that the user may have accidentally switched the order of the values. Reverting these function makes it easier to detect such errors.
The SafeguardEntry
and SafeguardEditEntry
structs are used in the addSafeguard
and editSafeguard
functions:
struct SafeguardEntry {
TokenType tokenType;
address contractAddress;
uint256 amount;
uint256 tokenId;
}
struct SafeguardEditEntry {
bytes32 key;
uint256 newTokenId;
uint256 newAmount;
}
For example, the tokenId
parameter is not checked in the ERC20 branch of the addSafeguard
function:
SafeguardEntry memory incomingSafeguard = _ercEntries[index];
if (incomingSafeguard.contractAddress == address(0)) {
revert ZeroAddress();
}
// If registry is enabled and the contract is a safe + supported address
if (
registryEnabled &&
!registry.isContractSupported(incomingSafeguard.contractAddress)
) {
revert InvalidContractAddress();
}
bytes32 key = encodeKey(currentKey, incomingSafeguard.tokenType);
if (incomingSafeguard.tokenType == TokenType.ERC20) {
ERC20SafeguardInfo storage safeguard = erc20Safeguards[key];
if (safeguard.contractAddress == address(0)) {
safeguard.contractAddress = incomingSafeguard
.contractAddress;
safeguard.amount = incomingSafeguard.amount;
emit ERC20SafeguardAdded(key);
unchecked {
++currentKey;
}
}
}
// Informational
The Saferoot
contract uses the Initializable
module from OpenZeppelin, and the implementations of this contract are not marked as initialized in the constructor. An uninitialized instance can be initialized by anyone to take over the contract. Even if it does not affect the instances deployed by the SaferootFactory
, it is still a good practice to prevent the initialization of the implementation contracts to avoid misuse. In the latest versions, this is done by calling the _disableInitializers
function in the constructor.
SOLVED: The \client team solved the issue in commit d1df49e by including a constructor to automatically mark the contracts as initialized.
// Informational
It was identified that there is no limit in the Saferoot
contract on how many safeguards can be added by the user. Adding too many safeguards may prevent the service user to efficiently monitoring the system and call safeguards in time.
Note that the off-chain backend system was outside this security assessment's scope.
ACKNOWLEDGED: The \client team acknowledged this finding and will perform tests to identify the limitations of the service and manage the number of active safeguards off-chain.
// Informational
It was identified that the Saferoot
contract may not be compatible with tokens that do not follow the ERC standards if these tokens do not implement the functions (e.g., getApproved
, safeTransferFrom
) that the Saferoot
contract uses to verify permissions for token transfers or transfer tokens to the backup address (e.g., CryptoKitties and CryptoPunks NFTs).
ACKNOWLEDGED: The \client team acknowledged this finding. They are going to create an off-chain database with the supported tokens, and they will notify the users about these limitations.
// Informational
It was identified that multiple safeguards can be added to the same asset in the Saferoot
contract. Adding multiple safeguards for the same asset may prevent the backend systems from correctly identifying which safeguards should be called.
Note that the backend systems were outside this security assessment's scope.
SOLVED: The \client team solved the issue in commit 837c806 by modifying the design of the Saferoot
contract.
// Informational
The initiateSafeguard
function transfers the tokens from the user's wallet to the backup address based on the added safeguards. The keys of the safeguards to use are given in the function's _safeguardKeys
array parameter. The function iterates through these keys and gets the data of the safeguards from the associated mappings, transferring the specified tokens from the user to the backup address. However, this involves looping over multiple array items, querying the required data, calling external contracts, and emitting events.
The execution of these actions requires a certain amount of gas based on how much computation is needed to complete them. Adding too many keys to the _safeguardKeys
parameter may max out the gas limit, reverting the initiateSafeguard
function, which prevents transferring the user's tokens in time.
The initiateSafeguard
function is looping over its dynamic array parameter:
function initiateSafeguard(bytes32[] calldata _safeguardKeys)
external
payable
onlyService
nonReentrant
{
for (uint256 index = 0; index < _safeguardKeys.length; ++index) {
bytes32 key = _safeguardKeys[index];
TokenType tokenType = decodeKeyTokenType(key);
if (tokenType == TokenType.ERC20) {
ERC20SafeguardInfo memory safeguard = erc20Safeguards[key];
address contractAddress = safeguard.contractAddress;
if (contractAddress != address(0)) {
// Transfer token
if (_transfer20(key, contractAddress, safeguard.amount)) {
emit SafeguardInitiated(key);
}
// If the contract address is already set, then skip
continue;
}
}
ACKNOWLEDGED: The \client team acknowledged this finding.
// Informational
It was identified that the Saferoot
, SaferootFactory
, and ContractRegistry
contracts are missing functions to sweep/recover accidental ERC20 token and Ether transfers. Mistakenly sent tokens and Ether are locked in the contracts indefinitely.
SOLVED: The \client team solved the issue in commit 910d794 by adding a function to enable the user to recover accidental ERC20 token transfers and removing the payable
functions to prevent accidental Ether transfers. Note that only the user can withdraw accidental token transfers from the Saferoot
contract.
// Informational
It was identified that the for loops employed in the contracts can be gas optimized by the following principles:
Unnecessary reading of the array length on each iteration wastes gas.
A postfix (e.g. i++
) operator was used to increment the i
variables. It is known that, in loops, using prefix operators (e.g. ++i
) costs less gas per iteration than postfix operators. It is also possible to further optimize loops by using unchecked loop index incrementing and decrementing.
Note that view or pure functions only cost gas if they are called from on-chain.
contracts/ContractRegistry.sol
for (uint256 i = 0; i < _contracts.length; i++) {
contracts/Saferoot.sol
for (uint256 index = 0; index < _safeguardKeys.length; ++index) {
for (uint256 index; index < _ercEntries.length; ++index) {
for (uint256 index = 0; index < _ercEditEntries.length; ++index) {
The original and the optimized gas cost of the updateSupportedContracts
function were compared to measure its gas efficiency:
Original: 162236
Optimized: 161833
Difference: 403
SOLVED: The \client team solved the issue in commit 0914a6b by applying the above recommendations.
// Informational
It was identified that the addSafeguard
function in the Saferoot
contract employs unnecessary validation to check if the currentKey
is already used or not. However, because the currentKey
is increased every time a safeguard is added, this condition is always true.
bytes32 key = encodeKey(currentKey, incomingSafeguard.tokenType);
if (incomingSafeguard.tokenType == TokenType.ERC20) {
ERC20SafeguardInfo storage safeguard = erc20Safeguards[key];
if (safeguard.contractAddress == address(0)) {
safeguard.contractAddress = incomingSafeguard
.contractAddress;
safeguard.amount = incomingSafeguard.amount;
emit ERC20SafeguardAdded(key);
unchecked {
++currentKey;
}
}
} else if (incomingSafeguard.tokenType == TokenType.ERC721) {
ERC721SafeguardInfo storage safeguard = erc721Safeguards[key];
if (safeguard.contractAddress == address(0)) {
safeguard.contractAddress = incomingSafeguard
.contractAddress;
safeguard.tokenId = incomingSafeguard.tokenId;
emit ERC721SafeguardAdded(key);
unchecked {
++currentKey;
}
}
} else if (incomingSafeguard.tokenType == TokenType.ERC1155) {
ERC1155SafeguardInfo storage safeguard = erc1155Safeguards[key];
if (safeguard.contractAddress == address(0)) {
safeguard.contractAddress = incomingSafeguard
.contractAddress;
safeguard.tokenId = incomingSafeguard.tokenId;
safeguard.amount = incomingSafeguard.amount;
emit ERC1155SafeguardAdded(key);
unchecked {
++currentKey;
}
}
}
SOLVED: The \client team solved the issue in commit bc0e505 by removing the unnecessary checks from the addSafeguard
function.
// Informational
It was identified that the contracts are using Solidity version 0.8.20 with its default Shanghai EVM version. The Shanghai fork introduced the PUSH0
opcode and was only supported on Mainnet, Goerli and Sepolia at the time of the assessment.
ACKNOWLEDGED: The \client team acknowledged this finding. The \client team will adjust the EVM version when deploying to new ecosystems.
In the manual testing phase, the following scenarios were simulated. The scenarios listed below were selected based on the severity of the vulnerabilities Halborn was testing the program for.
Proper access control on privileged functions was tested. All functions were reviewed to ensure that no sensitive functionality was left unprivileged.
USER_ROLE
can add or edit safeguards, modify the backup wallet address or disable the registry in the Saferoot
contract. This role is configured during initialization, and it is not possible to change it or assign it to any other users afterward.SERVICE_ROLE
can initiate safeguards. This role is configured during initialization, and it is not possible to change it or assign it to any other users afterward.ContractRegistry
contract.SaferootFactory
contract to create Saferoot
contracts.It was tested that the smart contract functionalities operate as intended.
encodeKey
, decodeKeyTokenType
and decodeKeyID
functions used in the Saferoot
contract are working as intended.user
to the backup
address. The Saferoot
contract cannot transfer the assets to any other address.It was tested that the deployments of the Saferoot
contracts with the SaferootFactory
contract operate as intended.
Saferoot
contracts were configured properly.Saferoot
contracts were deployed and initialized in the same transaction, preventing any malicious user to front run the initialize
transaction.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.
contracts/Saferoot.sol
\input{slither/Saferoot}
contracts/SaferootFactory.sol
\input{slither/SaferootFactory}
contracts/ContractRegistry.sol
Slither did not identify any vulnerabilities in the contract.
contracts/ErrorReporter.sol
Slither did not identify any vulnerabilities in the contract.
The findings obtained as a result of the Slither scan were reviewed. The medium-risk and reentrancy vulnerabilities were not included in the report because they were determined false positives.
Halborn used automated security scanners to assist with detection of well-known security issues and to identify low-hanging fruits on the targets for this engagement. Among the tools used was MythX, a security analysis service for Ethereum smart contracts. MythX performed a scan on the smart contracts and sent the compiled results to the analyzers in order to locate any vulnerabilities.
contracts/Saferoot.sol
contracts/SaferootFactory.sol
contracts/ErrorReporter.sol
contracts/ContractRegistry.sol
The findings obtained as a result of the MythX scan were examined, and they were not included in the report because they were determined false positives.
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