Prepared by:
HALBORN
Last Updated 01/28/2025
Date of Engagement: January 10th, 2025 - January 14th, 2025
100% of all REPORTED Findings have been addressed
All findings
10
Critical
0
High
0
Medium
0
Low
2
Informational
8
Plume
engaged Halborn
to conduct a security assessment on their Aidrop Distributor and Staking Solidity smart contracts beginning on January 10th, 2025 and ending on January 14th, 2025. The security assessment was scoped to the smart contracts provided in the plume-contracts GitHub repository, commit hashes, and further details can be found in the Scope section of this report.
Plume
is a public blockchain that tokenizes real-world assets through DeFi, enhancing their accessibility and liquidity for investors.
The team at Halborn assigned one full-time security engineer to check the security of the smart contracts. The security 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 this assessment is to:
Ensure that smart contract functionality operates as intended
Identify potential security issues with the smart contracts
In summary, Halborn identified some improvements to reduce the likelihood and impact of risks, which were accepted and acknowledged by the Plume team
. The main ones were the following:
Prevent fund locking by explicitly defining deployment modes during contract initialization and disallowing transitions that could restrict user access to funds.
Ensure logical consistency in deployment settings by validating parameters, such as enforcing restrictions only in appropriate deployment modes, to avoid unintended behaviors.
Validate all critical address inputs in the constructor to prevent zero-address vulnerabilities that may lead to misdirected operations or contract failures.
Enforce a fixed signature length of 65 bytes in signature validation to ensure proper handling of ECDSA signatures and prevent potential misuse.
Use consistent and exact compiler versions across related contracts to ensure compatibility and avoid unexpected behavior from version mismatches.
Halborn
performed a combination of manual review of the code and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of the smart contract assessment. While manual testing is recommended to uncover flaws in logic, process, and implementation; automated testing techniques help enhance coverage of smart contracts 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 the architecture, purpose, and use of the platform.
Smart contract manual code review and walkthrough to identify any logic issue.
Thorough assessment of safety and usage of critical Solidity variables and functions in scope that could lead to arithmetic related vulnerabilities.
Manual testing by custom scripts.
Graphing out functionality and contract logic/connectivity/functions (solgraph
).
Static Analysis of security for scoped contract, and imported functions. (Slither
).
EXPLOITABILITY 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
8
Security analysis | Risk level | Remediation Date |
---|---|---|
Potential Fund Locking When Changing permittedStaker | Low | Risk Accepted - 01/25/2025 |
Lack of Logical Enforcement in Unlock Parameter Settings | Low | Risk Accepted - 01/25/2025 |
Missing Validation for Zero Addresses in Constructor | Informational | Acknowledged - 01/25/2025 |
Lack of Signature Length Enforcement | Informational | Acknowledged - 01/25/2025 |
Inconsistent and Floating Pragma Version | Informational | Acknowledged - 01/25/2025 |
Lack of Events for Main State Changes | Informational | Acknowledged - 01/25/2025 |
Lack of NatSpec Documentation for Functions | Informational | Acknowledged - 01/25/2025 |
Inefficient Logic in Stake Processing | Informational | Acknowledged - 01/25/2025 |
Poor Traceability in Error | Informational | Acknowledged - 01/25/2025 |
Unused Parameters | Informational | Acknowledged - 01/25/2025 |
//
The setPermittedStaker
function from Staking contract allows the owner to change the permittedStaker
value, affecting who can stake and unstake funds.
When permittedStaker
is set to address(0)
, any user can freely stake and unstake. However, changing permittedStaker
from address(0)
to a valid address restricts staking and unstaking to the permittedStaker
. If this change is made after the contract has already been initialized in "open" mode and contains user funds, those funds could become inaccessible, effectively locking them without recourse.
Code of setPermittedStaker
function from Staking.sol contract.
function setPermittedStaker(address _permittedStaker) external onlyOwner {
permittedStaker = _permittedStaker;
}
To prevent unintended user fund locking, if the contract is deployed in "open" mode (permittedStaker == address(0)
), it should remain in this mode permanently.
It is recommended to set the permittedStaker
address during deployment via the constructor to explicitly define the deployment mode. Updates to the permittedStaker
should only be allowed if it was initially set to a non-zero address.
RISK ACCEPTED: The Plume team has acknowledged and accepted the risk associated with this finding.
//
The setUnlockParams
function permits non-zero lockingPeriodBlocks
even when permittedStaker == address(0)
.
This behavior could unintentionally impose a vesting period in open deployments where unrestricted staking and unstaking are expected. Enforcing a logical relationship between deployment settings ensures the contract operates as intended and prevents unnecessary restrictions on user actions.
Code of setUnlockParams
function from Staking.sol contract.
function setUnlockParams(
uint256 _unstakingStartBlock,
uint256 _lockingPeriodBlocks
) external onlyOwner {
unstakingStartBlock = _unstakingStartBlock;
lockingPeriodBlocks = _lockingPeriodBlocks;
}
It is recommended to add a validation in the setUnlockParams
function to enforce that parameters remain consistent with the deployment mode, ensuring no unintended restrictions are applied in open deployments.
Alternatively, an enforce mechanism can be implemented in the unstake
function to automatically set the value to 0
if the deployment is in open mode, preventing unnecessary locking periods.
RISK ACCEPTED: The Plume team has acknowledged and accepted the risk associated with this finding.
//
The constructors of both the Distributor.sol
and Staking.sol
contracts do not check if critical addresses (e.g., _signer
, _token
, _staking
, _stakingToken
) are zero.
Deploying contracts with zero addresses could lead to unexpected behavior or vulnerabilities, such as misdirected operations or inability to use the contract as intended.
It is recommended to add explicit checks in the constructor to revert if any of the provided addresses are zero.
ACKNOWLEDGED: The Plume team has acknowledged this finding.
//
The _signatureCheck
function in the Distributor.sol
contract does not enforce a fixed signature length of 65 bytes, which is the required length for ECDSA signatures.
Accepting invalid or malformed signatures could lead to unexpected behavior, such as failing to validate legitimate transactions.
Code of _signatureCheck
function from Distributor.sol contract.
function _signatureCheck(
bytes32 _messageHash,
bytes calldata _signature
) internal view {
if (_signature.length == 0) revert InvalidSignature();
bytes32 prefixedHash = ECDSA.toEthSignedMessageHash(_messageHash);
address recoveredSigner = ECDSA.recoverCalldata(
prefixedHash,
_signature
);
if (recoveredSigner != signer) revert InvalidSignature();
}
It is recommended to replace the check if (_signature.length == 0)
with strict validation, enforcing the signature length to be exactly 65 bytes.
ACKNOWLEDGED: The Plume team has acknowledged this finding.
//
The Staking.sol
contract uses a floating pragma version (e.g., ^0.8.0
) and does not match the pragma version of the Distributor.sol
contract.
Floating pragmas can lead to unexpected behavior due to differences in compiler versions, and mismatched pragma versions between related contracts may introduce compatibility issues.
Code from Staking.sol contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
It is recommended to set the pragma version in Staking.sol
to exactly match the one in Distributor.sol
(e.g., pragma solidity 0.8.25;
). Avoid using floating pragmas to ensure consistency and reliability during compilation.
ACKNOWLEDGED: The Plume team has acknowledged this finding.
//
In both contracts, important operations such as unlock
, setSigner
, withdrawTokens
, setClaimRoot
(in Distributor.sol
) and setPermittedStaker
, setUnlockParams
(in Staking.sol
) lack corresponding events.
This omission makes it difficult to track state changes, monitor operations, or debug the contract effectively, reducing transparency and auditability.
It is recommended to introduce events for all main state-changing operations. Emit these events with appropriate parameters to log the changes in a structured and traceable manner.
ACKNOWLEDGED: The Plume team has acknowledged this finding.
//
Several functions in the Distributor.sol
and Staking.sol
contracts lack complete or accurate NatSpec documentation, reducing clarity and usability for developers and external tools.
Missing tags such as @notice
, @dev
, @param
, and @return
make it harder to understand the purpose, behavior, and inputs/outputs of the functions.
Affected Functions:
In Distributor.sol
:
unlock
toggleActive
In Staking.sol
:
constructor
setPermittedStaker
setUnlockParams
unstake
toggleActive
getStakeInfo
It is recommended to ensure that all public and external functions include comprehensive NatSpec documentation with all relevant tags to improve clarity, usability, and integration with automated tools.
ACKNOWLEDGED: The Plume team has acknowledged this finding.
//
The logic in the stake
function includes two inefficiencies:
Redundant Balance Comparison: The function calculates stakingAmount
by comparing the contract’s token balance before and after the transfer. This approach is unnecessary and gas-inefficient. Instead, the _amount
parameter can be used directly, or it can be replaced with more accurate validations, such as checking the sender's balance and allowance.
Unnecessary Conditional Update: The user’s stake (userStake.amount
) is updated with conditional logic based on whether userStake.amount > 0
. This separation is redundant, as a direct addition would achieve the same result with less complexity.
Code of stake
function from Staking.sol contract.
Stake storage userStake = stakeInfo[_beneficiary];
uint256 balanceBefore = IERC20(stakingToken).balanceOf(address(this));
IERC20(stakingToken).safeTransferFrom(
msg.sender,
address(this),
_amount
);
uint256 balanceAfter = IERC20(stakingToken).balanceOf(address(this));
uint256 stakingAmount = balanceAfter - balanceBefore;
// Update user's stake
if (userStake.amount > 0) {
userStake.amount += stakingAmount;
} else {
userStake.amount = stakingAmount;
}
totalStaked += stakingAmount;
It is recommended to:
Replace the balance comparison logic with either directly using the _amount
parameter or validating the sender’s balance and allowance beforehand to ensure they have sufficient tokens and approval.
Simplify the logic for updating userStake.amount
by directly adding the staking amount to the existing value, regardless of whether it is initially zero.
ACKNOWLEDGED: The Plume team has acknowledged this finding.
//
The NotPermittedStaker
error is reused for various scenarios in the unstake
function, including cases where _onBehalfOf
is invalid.
This lack of specificity makes debugging and tracing errors more difficult.
Code of unstake
function from Staking.sol contract.
if (permittedStaker != address(0)) {
if (permittedStaker != msg.sender) {
revert NotPermittedStaker();
}
} else {
_unlockDelayReduction = 0;
if (msg.sender != _onBehalfOf) {
revert NotPermittedStaker();
}
}
It is recommended to introduce specific and descriptive errors for each failure scenario, such as InvalidOnBehalfOf
, to clearly differentiate the issues.
ACKNOWLEDGED: The Plume team has acknowledged this finding.
//
The Stake
struct declared in the IStaking.sol
interface contains parameters lastAccruedBlock
and accruedInterest
, which are not used in any logic or function in the Staking.sol
contract.
Including unused parameters increases gas costs, reduces code clarity, and may mislead developers regarding their purpose.
Code from IStaking.sol interface:
struct Stake {
uint256 amount; // Amount of tokens staked
uint256 lastAccruedBlock; // Block number when stake was created/last updated
uint256 accruedInterest;
}
It is recommended to remove the unused parameters lastAccruedBlock
and accruedInterest
from the Stake
struct if they are not necessary for future functionality.
ACKNOWLEDGED: The Plume team
has acknowledged this finding.
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 from the Slither scan have not been included in the report, as they were all related to third-party dependencies or 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