Prepared by:
HALBORN
Last Updated 07/06/2024
Date of Engagement by: June 5th, 2024 - June 12th, 2024
100% of all REPORTED Findings have been addressed
All findings
8
Critical
0
High
0
Medium
0
Low
2
Informational
6
Plural Energy
engaged Halborn
to conduct a security assessment of their Plural Protocol
beginning on June 5th and ending on June 12th. The security assessment was scoped to three updated smart contracts provided in the Plural-Energy's GitHub repository. Commit hash and further details can be found in the Scope section of this report.
In Plural Protocol
contract updates, we find:
The Secondary
contract is a decentralized exchange (DEX) that allows users to buy and sell tokens on the Plural protocol.
The AssetToken
contract is an implementation of a fungible token on the Ethereum blockchain that represents assets within the Plural protocol.
The Offering
contract is responsible for managing token sales on the Plural protocol. It allows authorized entities (such as project teams or investment funds) to create and manage token offerings.
Halborn
was provided 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 mostly addressed by the Plural Energy 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
).
Symbolic Analysis (Halmos
).
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
6
Security analysis | Risk level | Remediation Date |
---|---|---|
User deposits can be burned | Low | Solved - 06/18/2024 |
changeDepositories can 'hide' users' deposits | Low | Solved - 06/18/2024 |
Open TODO in comments | Informational | Solved - 06/18/2024 |
Redundant Code in UUPSUpgradeable Override | Informational | Solved - 06/18/2024 |
Lack of Zero Checks for Amounts | Informational | Solved - 06/18/2024 |
Lack of event emission during state changes | Informational | Solved - 06/18/2024 |
Use of unchecked in returnDeposit Function | Informational | Acknowledged |
Inappropriate Condition Check for OpenZeppelin's ERC20 Implementation | Informational | Solved - 06/18/2024 |
// Low
The burn
function has the potential to burn tokens deposited by users into the "depository" address. This unintended behavior can result in the loss of user funds, undermining trust and integrity in the contract.
Besides, if tokens are burned incorrectly, the user's balance ownedBalanceOf
could become negative, causing an underflow error. This compromises the accuracy of balance tracking and poses significant risks to contract stability and security. Implementing proper safeguards in the burn
function is essential to prevent these underflow issues and ensure reliable balance management.
Following test have been made to prove this issue:
function test_Burn_Deposited(address _holder, uint256 _amount) public {
vm.assume(_holder != address(0));
vm.assume(_holder != _depository);
vm.assume(_amount != 0);
_forceNotPlural(_holder);
test_TakeDeposit_Working(_holder, _amount);
vm.prank(_manager);
assetToken.burn(_depository, _amount);
assertEq(assetToken.balanceOf(_holder), uint256(0));
assertEq(assetToken.ownedBalanceOf(_holder), _amount);
assertEq(assetToken.balanceOf(_depository), 0);
assertEq(assetToken.ownedBalanceOf(_depository), uint256(0));
}
With following result
To prevent this, it is crucial to implement checks ensuring that only eligible tokens are burned, safeguarding user deposits and maintaining the contract's reliability.
SOLVED: The Plural Energy team added a check in order to confirm if the user from
has still funds deposited to avoid burning tokens.
// Low
The changeDepositories
function alters the accounts designated for storing user deposits. If deposits exist in an old account, these deposits will be "hidden" from user balances once the depository is changed. This can lead to discrepancies in user balances, causing confusion and potential loss of funds visibility.
Following tests confirm this issue:
function test_ReturnDeposit_ToHolder_DeprecatedDepository(address _newDepository, address _holder, uint256 _amount) public {
vm.assume(_holder != address(0));
vm.assume(_holder != _depository);
vm.assume(_newDepository != _depository);
vm.assume(_amount != 0);
test_TakeDeposit_Working(_holder, _amount);
vm.prank(_manager);
assetToken.changeDepositories(toDyn([_newDepository]));
assertEq(assetToken.balanceOf(_holder), uint256(0));
assertEq(assetToken.ownedBalanceOf(_holder), _amount);
assertEq(assetToken.balanceOf(_depository), _amount);
assertEq(assetToken.ownedBalanceOf(_depository), uint256(0));
assertEq(assetToken.balanceOf(_newDepository), 0);
assertEq(assetToken.ownedBalanceOf(_newDepository), uint256(0));
vm.prank(_depository);
assetToken.returnDeposit(_holder, _holder, _amount);
assertEq(assetToken.depositFrom(_holder), uint256(0));
assertEq(assetToken.depositTo(_depository), uint256(0));
}
function test_ReturnDeposit_ToHolder_NewDepository(address _newDepository, address _holder, uint256 _amount) public {
vm.assume(_holder != address(0));
vm.assume(_holder != _depository);
vm.assume(_newDepository != _depository);
vm.assume(_amount != 0);
test_TakeDeposit_Working(_holder, _amount);
vm.prank(_manager);
assetToken.changeDepositories(toDyn([_newDepository]));
assertEq(assetToken.balanceOf(_holder), uint256(0));
assertEq(assetToken.ownedBalanceOf(_holder), _amount);
assertEq(assetToken.balanceOf(_depository), _amount);
assertEq(assetToken.ownedBalanceOf(_depository), uint256(0));
assertEq(assetToken.balanceOf(_newDepository), 0);
assertEq(assetToken.ownedBalanceOf(_newDepository), uint256(0));
vm.prank(_newDepository);
assetToken.returnDeposit(_holder, _holder, _amount);
assertEq(assetToken.depositFrom(_holder), uint256(0));
assertEq(assetToken.depositTo(_depository), uint256(0));
}
With following results:
To prevent this, it's crucial to implement a mechanism to transfer existing deposits to the new depository or properly account for them during the transition, ensuring accurate balance tracking and user trust.
SOLVED: The Plural Energy team added convenient checks and events to prevent changing depositories until they are empty.
// Informational
The codebase contains TODOs and developer notes, indicating incomplete features or areas requiring further attention. These placeholders suggest that the contract may not be fully implemented or tested, posing potential risks for deployment.
Addressing these TODOs and reviewing the notes is crucial to ensure the contract's functionality, security, and readiness for production. Proper documentation and resolution of these items are essential to maintain code quality and reliability.
SOLVED: The Plural Energy team has removed the comment from the codebase.
// Informational
The contract overrides the _upgradeTo
function from UUPSUpgradeable
to add the onlyOwner
modifier, but it also overrides _authorizeUpgrade
with the same purpose. This results in redundant code, as both functions are essentially performing the same access control check.
To streamline the code and improve maintainability, it is recommended to use only _authorizeUpgrade
for the ownership check and remove the redundancy in _upgradeTo
. This approach maintains the necessary security while reducing code duplication and potential errors.
SOLVED: The Plural Energy team proceed to delete _upgradeTo
overrided function.
// Informational
The contract lacks validation checks for zero amounts in its functions. Failing to verify that amounts are non-zero can lead to unintended behavior, such as unnecessary transactions or logic execution. Implementing zero checks ensures that only meaningful and valid transactions are processed, enhancing contract efficiency, security, and reliability. This preventive measure reduces potential errors and maintains the integrity of the contract’s operations.
To address this issue, add checks to ensure amounts are greater than zero before proceeding with function logic.
Example implementation:
function transfer(address recipient, uint256 amount) public returns (bool) {
require(amount > 0, "Amount must be greater than zero");
// existing transfer logic
}
By incorporating such checks, the contract ensures that only valid, non-zero transactions are processed, improving overall robustness and preventing unnecessary operations.
SOLVED: The Plural Energy team added convenient checks to their codebase.
// Informational
The contract AssetToken does not emit events during critical state changes in takeDeposit
nor returnDeposit
, resulting in a lack of transparency and traceability.
Event emissions are crucial for monitoring contract activity, debugging, and providing real-time updates to users and external systems. Implementing event emissions ensures that state changes are recorded on the blockchain, enabling better auditing and enhancing user trust.
To address this issue, include event emissions in functions where state changes occur.
Example implementation:
event StateChanged(address indexed user, uint256 newValue);
function changeState(uint256 newValue) public {
// existing state change logic
emit StateChanged(msg.sender, newValue);
}
By emitting events during state changes, the contract provides clear and transparent logs of its activities, improving overall accountability and reliability.
SOLVED: The Plural Energy team added convenient events to their codebase.
// Informational
The returnDeposit
function can modify depositFrom
and depositTo
using the unchecked
keyword because these values are already validated to prevent underflow.
Leveraging unchecked
in this context can optimize gas usage by avoiding redundant checks, given that the necessary validations are already in place.
Example implementation demonstrating the safe use of unchecked
:
function returnDeposit(
address holder,
address tokenRecipient,
uint amount
) public override onlyDepository {
// Validate
if (holder == address(0)) revert ZeroAddress();
if (tokenRecipient == address(0)) revert ZeroAddress();
if (depositFrom[holder] < amount) revert InsufficientDeposit(holder);
if (depositTo[msg.sender] < amount)
revert InsufficientDeposit(msg.sender);
........
unchecked {
depositFrom[holder] -= amount;
depositTo[msg.sender] -= amount;
}
_transfer(tokenRecipient, amount, false);
}
By implementing unchecked
in the returnDeposit
function after proper validation, the contract enhances efficiency without compromising security.
ACKNOWLEDGED: The Plural Energy team acknowledged this finding.
// Informational
Methods _transfer
and _transferFrom
are using a condition to check the outcome of OpenZeppelin's ERC20 functions, which revert on failure instead of returning a boolean. This can lead to incorrect error handling and unexpected behavior. OpenZeppelin's ERC20 functions use revert to signal failures, making conditional checks ineffective for these cases. Proper error handling should be implemented to catch these reverts and handle them accordingly.
Error handling by using try/catch is a solution, but just receiving the revert coming from the ERC20, could be enough.
SOLVED: The Plural Energy team decided to relay in OpenZeppelin's ERC20 revert behavior so deleted the conditionals from their codebase.
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.
Results includes several security findings which were confirmed to be false positives or are already included in this report as findings.
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