Prepared by:
HALBORN
Last Updated 05/07/2024
Date of Engagement by: April 8th, 2024 - April 17th, 2024
100% of all REPORTED Findings have been addressed
All findings
5
Critical
0
High
0
Medium
0
Low
2
Informational
3
Jigsaw
engaged Halborn
to conduct a security assessment of their Jigsaw-lite
protocol beginning on April 8th and ending on April 17th. The security assessment was scoped to the smart contracts provided in the Jigsaw-lite GitHub repository. Commit hash and further details can be found in the Scope section of this report.
Jigsaw-lite
protocol incentivizes early users by rewarding their interactions before the full launch. Utilizing the [Ion protocol](https://ionprotocol.io), users earn yield by staking whitelisted underlying assets to Ion's pools. Additionally, participants receive jPoints, the protocol's reward token, which can later be exchanged for $Jig tokens, the governance token of Jigsaw
.
Halborn
was provided around one week for the engagement and assigned one full-time security engineer to review the security of the smart contract 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 in scope.
Ensure that smart contract functionality operates as intended.
In summary, Halborn identified some security recommendations that were successfully addressed by the Jigsaw 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 contracts (slither
, aderyn
and 4naly3er
).
Fork deployment (Foundry
).
External libraries, centralization risks and financial-related attacks.
New features/implementations after/with the remediation commit IDs.
Changes that occur outside the scope of PRs.
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
2
Informational
3
Security analysis | Risk level | Remediation Date |
---|---|---|
Centralization Risk | Low | Solved - 04/27/2024 |
addRewards function miscalculates rewardRate | Low | Solved - 04/27/2024 |
Access controlled functions with wrong visibility | Informational | Solved - 04/27/2024 |
Modifiers redundancy | Informational | Solved - 04/27/2024 |
Lack of checks-effects-interactions pattern | Informational | Solved - 04/27/2024 |
// Low
Despite being protected by access control measures, the invokeHolding(address _holding, address _contract, bytes calldata _call)
and function genericCall(address _contract, bytes calldata _call)
functions still warrant scrutiny due to its potential centralization risk within the Solidity codebase. While restricted to authorized insiders such as developers and administrators of the protocol, the function's ability to execute arbitrary code through external function calls introduces centralized control concerns.
Although access to the call
method is limited to privileged individuals, its invocation remains a centralized point of influence. Authorized insiders have the capability to trigger dynamic function calls, enabling them to influence the behavior of the contract beyond its predefined scope. While this access is crucial for protocol management and maintenance, it also poses inherent risks that must be carefully managed.
Centralization risks associated with these functions include:
Unauthorized Manipulation: While restricted to insiders, unauthorized or malicious use of the call
method could still lead to manipulation of the contract's state or execution flow, potentially resulting in unintended consequences or financial loss.
Privilege Escalation: Despite access controls, the possibility of privilege escalation exists, whereby insiders abuse their authorized status to perform actions beyond their intended scope, compromising the integrity of the contract.
Exposure to External Threats: External attackers may attempt to compromise authorized accounts or exploit vulnerabilities in the access control mechanism to gain unauthorized access to the call
method, posing a significant security risk to the protocol.
While access control measures provide a level of assurance, they should be complemented by proactive security measures to safeguard the integrity and resilience of the protocol against potential exploits and centralized control risks.
One possible solution is to entirely remove these centralized functions. In case this arbitrary call function is used in response to unexpected actions over the mentioned contract, deploying beacon-proxy clones offers enhanced flexibility.
SOLVED: The Jigsaw team implemented a custom solution which avoids the use of genericCall
without user permission. Even, the Jigsaw's user will have the opportunity to select who can use it and which contract and how many times it can be called.
// Low
addRewards
lacks of any tool to confirm that available funds are assigned to a certain rewardPeriod
or still pending to transfer/assign. This, in certain situations, can lead to miscalculations of the rewardRate
.
Although this issue can be solved in most situations by adding funds to Staker contract as soon as possible, failure to update accurate reward calculations could lead to:
Loss of user trust and decreased participation.
Negative impact on reputation and potential legal risks.
Economic imbalance within the protocol, causing dissatisfaction among users.
There are only 3 steps:
Owner of the contract transfer rewardTokens
the Staker
contract.
Owner of the contract call addRewards
in order to distribute rewards among existing and incoming users, with rewardsDuration
= 1 year.
6 months later, owner call to addRewards
again but this time he decide to only add the half of what was already added at the begining. So calculation of rewardRate
starts:
Lower limit is defined to be above 0:
- Calculated leftover
is the half of initial. (because the half of rewardsDuration
already passed)
- As _amount
, we again add the half of the initial amount, so (leftover
+ _amount
) == initial amount.
- Duration hasn't change.
- So finally, rewardRate
stays the same.
For upper limit we get that rewardToken
balance didn't change:
- Nobody withdrawn rewards since the beginning so apparently there are enough tokens to share, the initial amount.
Finally Stacker contract:
Renews rewardRate
as same value.
Renews periodFinish
by adding 1 year to current timestamp.
Consequence:
As no rewardToken
has been actually added to the contract, It will run out of funds when day 365 (from the beginning) arrives.
Last 6 months rewards will be virtually accrued but, if not detected and solved, unavailable in the contract.
function test_addRewards_withoutTransferingRewardTokens() public {
uint256 amount2add = rewardsDuration * 0.01 ether;
vm.prank(OWNER, OWNER);
bool approved = JigsawPoints(rewardToken).approve(address(staker), type(uint256).max);
require(approved, "approval failed");
vm.prank(OWNER, OWNER);
JigsawPoints(rewardToken).transfer(address(staker), amount2add);
console.log("Add rewards by same amount");
vm.prank(OWNER, OWNER);
staker.addRewards(amount2add);
uint256 initial_rewardRate = staker.rewardRate();
console.log("6 months later...");
skip(365 days / 2);
assertEq(block.timestamp, 365 days / 2 + 1);
console.log("We add more rewards but tokens has ever been transferred");
console.log("(in either direction)");
vm.prank(OWNER, OWNER);
staker.addRewards(amount2add / 2);
uint256 final_rewardRate = staker.rewardRate();
uint256 balance_ = JigsawPoints(rewardToken).balanceOf(address(staker));
assertEq(final_rewardRate, initial_rewardRate);
assertEq(staker.periodFinish(), 365 days * 1.5 + 1);
}
It appears that the easiest way to handle this issue is to do actual transferFrom
with the same _amount
used to call the function to get the rewardTokens
into the contract atomically, this way, anytime a reward is added, it will be actually present in the contract.
SOLVED: The Jigsaw team introduced the aforementioned transferFrom
inside addRewards
function.
// Informational
Functions withdraw
and claimRewards
, have onlyStakingManager
access modifier, but those functions are not implemented in contract StakingManager to be directly called. Therefore, its visibility can be changed to internal.
Change visibility to internal
or implement functions to call these from StakingManager
contract.
SOLVED: The Jigsaw team solved this issue by turning both functions to internal, making several changes accordingly.
// Informational
AccessControlDefaultAdminRules
's constructor is already checking that provided _admin
address is different from address zero.
Having two modifiers in a Solidity method that essentially perform the same task of verifying that an address is not address(0) introduces inefficiency and redundancy into the codebase. This redundancy increases the complexity of the code and adds unnecessary overhead to the execution of the method. Maintaining multiple modifiers with duplicate functionality complicates code readability and may lead to confusion for developers trying to understand the purpose and behavior of the method. Additionally, redundant modifiers consume additional gas during contract execution, potentially impacting the overall cost and performance of transactions.
Consider deleting validAddress(_admin)
modifier.
SOLVED: The Jigsaw team moved from AccessControl
to Ownable
and suppressed the modifier redundancy.
// Informational
Utilizing the checks-effects-interactions pattern remains crucial even when a function incorporates the nonReentrant modifier. While the nonReentrant modifier effectively prevents reentrancy attacks by restricting multiple calls to the same function during execution, the checks-effects-interactions pattern provides additional security measures. This pattern involves segregating the code into distinct phases: checks to validate inputs, effects to update state variables, and interactions with external contracts.
By adhering to this pattern, developers can ensure that each function call performs necessary validations before modifying the contract state or interacting with external contracts. This approach enhances the robustness and reliability of the codebase, mitigating potential vulnerabilities and reducing the risk of unintended behavior or exploits.
Furthermore, incorporating the checks-effects-interactions pattern promotes code clarity and maintainability by organizing the logic into well-defined stages. This architectural design facilitates debugging, auditing, and future modifications, contributing to the overall security and stability of the smart contract system.
Therefore, while the nonReentrant modifier addresses specific reentrancy concerns, combining it with the checks-effects-interactions pattern provides a comprehensive defense mechanism against a wider range of security threats and ensures the integrity and resilience of the Solidity codebase.
To improve this issue, it suggested creating a temporary variable adding existing _totalSupply
and _amount
and, if the check is passed, changing the actual storage _totalSupply
value.
SOLVED: The Jigsaw team modified the code by first checking the condition by reading the storage and after confirmation, it is actually modified.
Halborn used automated testing techniques to enhance the coverage of certain areas of the smart contract in scope. Among the tools used was Slither, a Solidity static analysis framework. After Halborn verified the smart contract in the repository and was able to compile it correctly into their ABI and binary format, Slither was run against the contract. This tool can statically verify mathematical relationships between Solidity variables to detect invalid or inconsistent usage of the contract's API across the entire code-base.
The security team assessed all findings identified by the Slither software, and findings with severity Information
and Optimization
are excluded in the below results.
The findings obtained as a result of the Slither scans were reviewed, and they were not included in the report because they were determined false positives or actual optimization, far from being security issues.
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