Prepared by:
HALBORN
Last Updated 08/06/2024
Date of Engagement by: June 3rd, 2024 - June 13th, 2024
100% of all REPORTED Findings have been addressed
All findings
5
Critical
0
High
0
Medium
3
Low
0
Informational
2
Horizen Labs engaged Halborn to conduct a security assessment on their smart contracts beginning on June 3rd, 2024, and ending on June 13th, 2024. The security assessment was scoped to the smart contracts provided in the following GitHub repository:
The team at Halborn was provided one and a half weeks for the engagement and assigned a full-time security engineer to verify the security of the smart contracts. The security engineer is a blockchain and smart-contract security expert with advanced penetration testing, smart-contract hacking, and deep knowledge of multiple blockchain protocols.
The purpose of this assessment is to:
- Ensure that the smart contract functions operate as intended.
- Identify potential security issues within the smart contracts.
In summary, Halborn identified some security risks that were mostly addressed by the Horizen Labs team
.
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.
- Static Analysis of security for scoped contract, and imported functions. (Slither
)
- Testnet deployment. (Foundry
)
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
3
Low
0
Informational
2
Security analysis | Risk level | Remediation Date |
---|---|---|
Call is recommended over transfer to send native assets | Medium | Solved - 06/19/2024 |
Checks-Effects-Interactions pattern is not followed in the claimReward() function | Medium | Solved - 06/19/2024 |
DelegatedStaking contract does not implement any receive or fallback function | Medium | Risk Accepted |
State variables missing immutable modifier | Informational | Solved - 06/19/2024 |
Unnecessary import | Informational | Solved - 06/19/2024 |
// Medium
The DelegatedStaking
contract sends Eon tokens to the delegators by making use of the Solidity transfer()
function:
function claimReward(address payable owner) external {
(uint256 totalToClaim, ClaimData[] memory claimDetails) = calcReward(owner);
if(totalToClaim == 0) {
revert NothingToClaim();
}
//transfer reward
owner.transfer(totalToClaim); // <----------------------
//update last claimed epoch
lastClaimedEpochForAddress[owner] = claimDetails[claimDetails.length - 1].epochNumber;
emit Claim(signPublicKey, forgerVrf1, forgerVrf2, owner, claimDetails);
}
In Solidity, the call()
function is often preferred over transfer()
for sending Ether In Solidity due to some gas limit considerations:
transfer
: Imposes a fixed gas limit of 2300 gas. This limit can be too restrictive, especially if the receiving contract is a multisig wallet that executes more complex logic in its receive()
function. For example, native transfer()
calls to Gnosis Safe multisigs will always revert with an out-of-gas error in Binance Smart Chain.
call
: Allows specifying a custom gas limit, providing more flexibility and ensuring that the receiving contract can perform necessary operations.
It should be noted that using call
also requires explicit reentrancy protection mechanisms (e.g., using checks-effects-interactions pattern or the ReentrancyGuard
contract from OpenZeppelin).
Consider using call()
over transfer()
to transfer native assets in order to ensure compatibility with any type of multisig wallet. Moreover, use the check-effects-interactions pattern in the DelegatedStaking.claimReward()
function to avoid any reentrancy risks.
SOLVED: The Horizen Labs team solved the issue by implementing the recommended solution.
// Medium
The DelegatedStaking
contract implements the claimReward()
function used by delegators to claim their rewards:
function claimReward(address payable owner) external {
(uint256 totalToClaim, ClaimData[] memory claimDetails) = calcReward(owner);
if(totalToClaim == 0) {
revert NothingToClaim();
}
//transfer reward
owner.transfer(totalToClaim); // <-----------------------------
//update last claimed epoch
lastClaimedEpochForAddress[owner] = claimDetails[claimDetails.length - 1].epochNumber; // <---------------
emit Claim(signPublicKey, forgerVrf1, forgerVrf2, owner, claimDetails);
}
As we can see in the code above, the check-effects-interactions pattern is not respected as the Eon transfer(interaction) is performed before the update of the lastClaimedEpochForAddress
state variable(effect). This could cause some reentrancy risks (which are partially mitigated by the 2300 gas limit of the transfer()
function).
It is recommended to update the DelegatedStaking.claimReward()
function as shown below:
function claimReward(address payable owner) external {
(uint256 totalToClaim, ClaimData[] memory claimDetails) = calcReward(owner);
if(totalToClaim == 0) {
revert NothingToClaim();
}
//update last claimed epoch
lastClaimedEpochForAddress[owner] = claimDetails[claimDetails.length - 1].epochNumber; // <---------------
//transfer reward
owner.transfer(totalToClaim); // <-----------------------------
emit Claim(signPublicKey, forgerVrf1, forgerVrf2, owner, claimDetails);
}
SOLVED: The Horizen Labs team solved the issue by implementing the recommended solution.
// Medium
The DelegatedStaking
contract is supposed to hold Eon tokens in order to allow users to execute their claims by calling the claimReward()
function. However, this contract does not implement any payable
function, nor a receive()
or fallback()
function.
Consider implementing a receive()
function in the DelegatedStaking
contract.
RISK ACCEPTED: The Horizen Labs team accepted the risk of this finding, and states that the smart contract will receive funds inserting its address as forger rewards receiver. In this case, fallback methods will not be required.
// Informational
In the contract DelegatedStaking
, the state variables signPublicKey
, forgerVrf1
and forgerVrf2
can be declared as immutable
to save some gas.
The immutable
keyword was added to Solidity in 0.6.5. State variables can be marked immutable
which causes them to be read-only, but only assignable in the constructor.
It is recommended to add the immutable
modifier to signPublicKey
, forgerVrf1
and forgerVrf2
state variables in the DelegatedStaking
contract.
SOLVED: The Horizen Labs team solved the issue by implementing the recommended solution.
// Informational
The DelegatedStaking
contract declares the following import:
import "hardhat/console.sol";
console.sol
is intended for debugging purposes and should not be part of the production code.
Remove the console.sol
import from the DelegatedStaking
contract.
SOLVED: The Horizen Labs team solved the issue by implementing the recommended solution.
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.
DelegatedStaking.sol
DelegatedStakingFactory.sol
The Ether/native assets sent to an arbitrary address issue is a false positive.
No major issues were found by Slither.
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