Prepared by:
HALBORN
Last Updated 09/16/2025
Date of Engagement: August 18th, 2025 - August 26th, 2025
100% of all REPORTED Findings have been addressed
All findings
6
Critical
0
High
0
Medium
0
Low
1
Informational
5
Decent Labs
engaged Halborn
to perform a security assessment of their smart contracts from August 18th, 2025, to August 26th, 2025. The assessment scope was limited to the smart contracts provided to Halborn. Commit hashes and additional details are available in the Scope section of this report.
The Decent Labs
codebase in scope consists of smart contracts implementing a modular token sale system with EIP-712-based buyer verification, a non-transferable ERC20 staking and rewards mechanism, and a system deployer contract for proxy deployments.
Halborn
was allocated 7 days for this engagement and assigned 1 full-time security engineer to conduct a comprehensive review of the smart contracts within scope. The engineer is an expert in blockchain and smart contract security, with advanced skills in penetration testing and smart contract exploitation, as well as extensive knowledge of multiple blockchain protocols.
The objectives of this assessment were to:
Identify potential security vulnerabilities within the smart contracts.
Verify that the smart contract functionality operates as intended.
In summary, Halborn
identified areas for improvement to reduce the likelihood and impact of potential risks, which were partially addressed by the Decent Labs team
:
Update the _distributeRewards() function to increment rewardsDistributed only by the amount actually allocated to stakers, not the full amountToDistribute.
Add a check in the initialize() function to ensure that protocolFeeReceiver is not the zero address.
Add a validation check in _validateHedgeyParams to ensure that cliff_ is greater than or equal to start_.
Halborn
conducted a combination of manual code review and automated security testing to balance efficiency, timeliness, practicality, and accuracy within the scope of this assessment. While manual testing is crucial for identifying flaws in logic, processes, and implementation, automated testing enhances coverage of smart contracts and quickly detects deviations from established security best practices.
The following phases and associated tools were employed throughout the term of the assessment:
Research into the platform's architecture, purpose and use.
Manual code review and walkthrough of smart contracts to identify any logical issues.
Comprehensive assessment of the safety and usage of critical Solidity variables and functions within scope that could lead to arithmetic-related vulnerabilities.
Local testing using custom scripts (Foundry
).
Fork testing against main networks (Foundry
).
Static security analysis of scoped contracts, and imported functions (Slither
).
After the conclusion of this assessment, the Decent Labs
team introduced a minor update to the VotesERC20Staked
contract in commit d7137b1 in order to support compatibility with both version 0 and version 1 deployments. Specifically, a clockModeTimestamp
boolean variable was added to storage and initialization, with corresponding updates to the CLOCK_MODE()
and clock()
functions. This change allows the contract to operate using either block timestamps or block numbers as the underlying clock mechanism.
Although this modification was introduced after the commit reviewed during the assessment and therefore falls outside the official scope of this engagement, Halborn
reviewed the update at a high level. Based on this review, it was concluded that the change is limited in scope and does not appear to introduce any new security risks.
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 (C:N) Low (C:L) Medium (C:M) High (C:H) Critical (C: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
1
Informational
5
Security analysis | Risk level | Remediation Date |
---|---|---|
Suboptimal reward accounting can permanently lock dust amounts in contract | Low | Risk Accepted - 09/02/2025 |
Missing validation for protocolFeeReceiver may result in lost funds | Informational | Acknowledged - 09/02/2025 |
Vesting schedule allows cliff to be set before vesting start | Informational | Solved - 09/02/2025 |
Staked token can be added as a reward token | Informational | Solved - 09/02/2025 |
Missing input validation | Informational | Acknowledged - 09/02/2025 |
Floating pragma | Informational | Acknowledged - 09/02/2025 |
//
The VotesERC20StakedV1::_distributeRewards()
function is responsible for distributing reward tokens to stakers. However, it increments the rewardsDistributed
variable by the full amountToDistribute
, even though the actual amount allocated to stakers (due to integer division truncation) may be slightly less. This discrepancy causes a small remainder ("dust") to remain in the contract balance after each distribution. Since rewardsDistributed
is incremented by the full amount, the contract's logic for _distributableRewards()
will never recognize this dust as available for future distributions, resulting in these tokens being permanently locked and unclaimable by stakers.
For example, if 10 reward tokens are distributed among 3 stakers, the rate increment is calculated as floor(10 * 1e18 / 3)
, resulting in each staker being able to claim 3.333...
tokens (in wei). The sum of all claimable rewards is 9.999...
tokens, leaving 1 wei permanently stranded in the contract.
Update the _distributeRewards()
function to increment rewardsDistributed
only by the amount actually allocated to stakers, not the full amountToDistribute
.
Specifically, after calculating the rate increment, compute the allocated amount as (rateIncrement * totalStaked) / PRECISION
and increment rewardsDistributed
by this value. This ensures that any dust remains available for future distributions and is never permanently locked.
Alternatively, track the dust in a dedicated variable and allow governance to sweep it if needed.
RISK ACCEPTED: The Decent Labs team made a business decision to accept the risk of this finding and not alter the contracts, stating:
Dust amounts are so small they can be considered negligible. Additional math to save dust likely costs more in gas than the value of the dust saved.
//
The TokenSaleV1
contract allows the protocolFeeReceiver
address to be set to the zero address during initialization. In both the sellerSettle()
and failed sale settlement paths, protocol fees are transferred to protocolFeeReceiver
without checking if it is the zero address.
If protocolFeeReceiver
is set to address(0)
, protocol fees will be irretrievably lost, as tokens sent to the zero address cannot be recovered. Any EOA or contract that initializes the sale with a zero address for protocolFeeReceiver
will cause this loss.
Add a check in the initialize()
function to ensure that protocolFeeReceiver
is not the zero address.
ACKNOWLEDGED: The Decent Labs team made a business decision to acknowledge this finding and not alter the contracts, stating:
We have made a conscious decision in the codebase to not check for address(0) upon contract initialization. There are a near infinite number of incorrect addresses the contract could be initialized with, and we can of course not check for them all.
//
The _validateHedgeyParams
function in the TokenSaleV1
contract does not check whether the cliff_
parameter is set before the start_
parameter. This allows a vesting schedule to be created where the cliff occurs before the vesting actually begins, which is illogical and could lead to confusion or unintended behavior.
For example, a sale could be initialized with start_ = 1000
and cliff_ = 900
, resulting in a cliff that is 100 seconds before vesting starts. This does not align with standard vesting logic, where the cliff should always be at or after the start of vesting.
Add a validation check in _validateHedgeyParams
to ensure that cliff_
is greater than or equal to start_
. For example:
if (cliff_ < start_) revert InvalidCliff();
SOLVED: The Decent Labs team solved this finding in the specified commit by following the mentioned recommendation.
//
The documentation states the “staked token itself cannot be added as a reward token,” but _addRewardsTokens()
only checks DuplicateRewardsToken()
and does not prevent token == stakedToken()
.
The owner could call addRewardsTokens([stakedToken])
after users have staked. While _distributableRewards()
special‑cases this by subtracting totalStaked
, the mismatch between stated invariant and code can mislead integrators, off‑chain accounting, or frontends that assume the array never contains the staked asset.
Enforce the documented rule by modifying the _addRewardsTokens()
function to revert if the token == stakedToken
.
SOLVED: The Decent Labs team solved this finding in the specified commit by following the mentioned recommendation, stating:
It is intended that the staked token may also be a reward token. In the attached PR, the natspec was updated to reflect this.
//
Throughout the TokenSaleV1
contract, several input parameters in the initialize()
function are assigned to state variables with only partial validation. While some checks are present, other critical parameters are not validated for correctness or safety. For example:
In TokenSaleV1.initialize()
, the following addresses are not checked against the zero address before assignment:
commitmentToken
saleToken
verifier
saleProceedsReceiver
protocolFeeReceiver
saleTokenHolder
hedgeyLockupParams.votingTokenLockupPlans
In VerifierV1.constructor()
, and the updateSigner()
function, the signer_
address is not checked against the zero address before assignment.
In VotesERC20StakedV1.initialize()
, the stakedToken_
address is not checked against the zero address before assignment.
In TokenSaleV1.initialize()
, saleStartTimestamp
and saleEndTimestamp
are only checked for ordering and not for equality, potentially allowing a zero-length sale.
In TokenSaleV1.initialize()
, minimumCommitment
and maximumCommitment
are only checked for ordering and not for equality or nonzero values, potentially allowing zero commitments.
In TokenSaleV1.initialize()
, minimumTotalCommitment
and maximumTotalCommitment
are only checked for ordering and not for equality or nonzero values, potentially allowing zero total commitments.
In TokenSaleV1.initialize()
, saleTokenPrice
is not validated to ensure it is greater than zero, which could lead to division by zero or pricing errors.
Add explicit validation to ensure that all critical input addresses are not the zero address and that numeric parameters are within reasonable, nonzero bounds. This will help prevent misconfiguration and improve the overall robustness of the contract.
ACKNOWLEDGED: The Decent Labs team made a business decision to acknowledge this finding and not alter the contracts, stating that:
We have made a conscious decision in this codebase to keep initialization parameter checks very minimal. Since there are many possible incorrect configurations, they cannot all be checked for. After reviewing the called out initialization parameters, we have to decided to leave this as is.
//
The contracts in scope use floating pragma version ^0.8.30
, which means that the code can be compiled by any compiler version that is greater than these versions, and less than 0.9.0
.
However, it is recommended that contracts should be deployed with the same compiler version and flags used during development and testing. Locking the pragma helps to ensure that contracts do not accidentally get deployed using another pragma. For example, an outdated pragma version might introduce bugs that affect the contract system negatively.
Additionally, from Solidity versions 0.8.20
through 0.8.24
, the default target EVM version is set to Shanghai
, which results in the generation of bytecode that includes PUSH0
opcodes. Starting with version 0.8.25
, the default EVM version shifts to Cancun
, introducing new opcodes for transient storage, TSTORE
and TLOAD
.
In this aspect, it is crucial to select the appropriate EVM version when it's intended to deploy the contracts on networks other than the Ethereum mainnet, which may not support these opcodes. Failure to do so could lead to unsuccessful contract deployments or transaction execution issues.
Lock the pragma version to the same version used during development and testing (for example: pragma solidity 0.8.30;
), and make sure to specify the target EVM version when using Solidity versions from 0.8.20
and above if deploying to chains that may not support newly introduced opcodes.
Additionally, it is crucial to stay informed about the opcode support of different chains to ensure smooth deployment and compatibility.
ACKNOWLEDGED: The Decent Labs team made a business decision to acknowledge this finding and not alter the contracts, stating that:
Since the solidity compiler is explicitly defined in the hardhat config file, a conscious decision is made when compiling with a newer solidity version. If the compiler version is incremented, and the tests all pass, then functionally it is fine to deploy with this newer version.
Halborn
used automated testing techniques to increase coverage of specific areas within the smart contracts under review. Among the tools used was Slither
, a Solidity static analysis framework. After Halborn
successfully verified the smart contracts in the repository and was able to compile them correctly into their ABI and binary formats, Slither
was executed against the contracts. This tool performs static verification of mathematical relationships between Solidity variables to identify invalid or inconsistent usage of the contracts' APIs throughout the entire codebase.
The security team reviewed all findings reported by the Slither
software; however, findings related to external dependencies have been excluded from the results below to maintain report clarity.
All findings identified by Slither
were proved to be false positives and therefore were not added to the issue list in this report.
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
Decent Contracts
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed