Introduction
We express our gratitude to the Justify team for the collaborative engagement that enabled the execution of this Smart Contract Security Assessment.
The Justify sale is a fixed-price token presale on Base. Buyers purchase with allow-listed stablecoins through TokenSale, funds settle directly to an immutable treasury address, and post-sale allocations are claimed from MerkleVestingClaim under a merkle-gated TGE, cliff, and linear or stepped vesting schedule.
Document | |
|---|---|
| Name | Smart Contract Code Review and Security Analysis Report for Justify |
| Audited By | Khrystyna Tkachuk |
| Approved By | Olesia Bilenka |
| Website | - |
| Changelog | 23/09/2026 - Preliminary Report |
| 28/09/2026 - Final Report | |
| Platform | Base |
| Language | Solidity |
| Tags | ERC20; Permit Token; Vesting; Token Sales; Centralization |
| Methodology | https://docs.hacken.io/methodologies/smart-contracts→ |
Document
- Name
- Smart Contract Code Review and Security Analysis Report for Justify
- Audited By
- Khrystyna Tkachuk
- Approved By
- Olesia Bilenka
- Website
- -
- Changelog
- 23/09/2026 - Preliminary Report
- 28/09/2026 - Final Report
- Platform
- Base
- Language
- Solidity
- Tags
- ERC20; Permit Token; Vesting; Token Sales; Centralization
Review Scope | |
|---|---|
| Repository | https://github.com/evacodes-dev/justify-sale→ |
| Commit | f6de3cf |
| Final Commit | ca1a19c |
Review Scope
- Commit
- f6de3cf
- Final Commit
- ca1a19c
Audit Summary
The system users should acknowledge all the risks summed up in the risks section of the report
Documentation quality
Functional requirements are sufficient.
Project overview is detailed.
All roles in the system are described.
Use cases are described.
Futures are described.
Technical description is detailed.
Run instructions are provided.
Technical specification is provided.
NatSpec is sufficient.
Code quality
The code leverages OpenZeppelin library, and follows the established pattern.
The codebase is well-structured and clearly organized.
The development environment is configured.
Test coverage
Code coverage of the project is 100%.
Deployment and basic user interactions are covered with 135 passing tests.
Negative cases coverage is present.
Interactions by several users are tested.
System Overview
The system comprises three non-upgradeable contracts: ProjectToken, TokenSale, and MerkleVestingClaim. ProjectToken is a fixed-supply ERC-20 with EIP-2612 Permit. The full TOTAL_SUPPLY of 1_000_000_000e18 is minted once to initialHolder at deployment. TokenSale accepts allow-listed stablecoins (USDC and USDT on Base, per deploy configuration), converts payment to 6-decimal USD and token entitlements at a fixed price (USD_DECIMALS = 6), and safeTransferFrom the payment from the payer to the immutable treasury in the same transaction so the sale contract holds no contribution balance. Purchases are recorded via contributions, tokensPurchased, and optional referral binding through referrerOf / referralBonusTokens. Buy entry points are buy, buyAtPrice, buyFor, and buyWithPermit. Dual ceilings (hardCapUsd, maxTokensForSale), pause by owner or guardian, and one-way finalize control the sale lifecycle. setSaleWindow may change both start and end only before startTime. After the sale opens, only setEndTime may adjust the end. Ownership uses Ownable2Step with renounceOwnership disabled. setGuardian remains callable after finalize.
After finalization, an off-chain merkle tool (per spec) rebuilds allocations from Purchase and Referral events. ProjectToken is transferred into MerkleVestingClaim, then setMerkleRoot, lockRoot, and setTge enable claims. Leaves follow OpenZeppelin StandardMerkleTree encoding of (account, totalAllocation). Unlock math in unlockedAt applies a TGE share (tgeBps), optional cliff, then continuous (unlockInterval = 0) or stepped vesting over vestingDuration. Claimants call claim with a merkle proof. After vesting ends plus SWEEP_DELAY (365 days), sweepUnclaimed returns leftovers to the treasury. Guardian freeze via setFrozen can block an account. Only the owner may unfreeze.
Files in Scope
TokenSale.sol: Presale accounting contract that accepts allow-listed stablecoins, forwards each payment to the immutable treasury, and records USD raised, tokens owed, and referral bonuses without issuing tokens on-chain. Exposes
buy,buyAtPrice,buyFor,buyWithPermit, admin setters for price, window, caps, currencies, and referral rate, pluspause/unpauseand one-wayfinalize.MerkleVestingClaim.sol: Merkle-gated vesting distributor for ProjectToken. After
lockRootand TGE,claimpays the unlocked portion of a leaf allocation under TGE, cliff, and linear or stepped release. Supports guardian freeze, ownersweepUnclaimedafter delay, and pre-lock rescue of the vested token.ProjectToken.sol: Fixed-supply ERC-20 (
TOTAL_SUPPLYof 1,000,000,000 tokens, 18 decimals) with ERC20Permit. The entire supply is minted toinitialHolderin the constructor. No further mint, owner, pause, or blacklist.
Privileged roles
TokenSale.sol
owner (inherited from Ownable2Step): Configures sale parameters while not finalized, controls pause state, finalizes the sale, sets the guardian (including after finalize), and rescues stray assets. Ownership transfer is two-step.
renounceOwnershipis disabled.Can call
setPriceto update the token price (reverts if finalized).Can call
setSaleWindowto set both start and end times before the sale has started (reverts if finalized or ifblock.timestamp >= startTime).Can call
setEndTimeto change the sale end time (reverts if finalized).Can call
setCapsto updatehardCapUsd,minPurchaseUsd, andmaxPerWalletUsd(reverts if finalized).Can call
setMaxTokensForSaleto update the token sale ceiling (reverts if finalized).Can call
setReferralBpsto update the referral rate for future purchases (reverts if finalized).Can call
setCurrencyto enable or disable an allow-listed payment token (reverts if finalized).Can call
setGuardianto set or clear the guardian address (available after finalize).Can call
pauseto pause the sale.Can call
unpauseto unpause the sale.Can call
finalizeto permanently finalize the sale (one-way).Can call
rescueERC20to transfer stray ERC20 tokens to the immutable treasury.Can call
rescueETHto transfer the contract ETH balance to the immutable treasury.Can call
transferOwnershipto nominate a new owner (pending owner must accept).Can call
renounceOwnership, which always reverts withRenounceDisabled.
guardian: May pause the sale. Cannot unpause, finalize, or change parameters.
Can call
pauseto pause the sale.
pendingOwner (inherited from Ownable2Step): Completes a two-step ownership transfer.
Can call
acceptOwnershipto accept ownership.
MerkleVestingClaim.sol
owner (inherited from Ownable2Step): Manages the Merkle root and TGE, freezes and unfreezes accounts, sweeps leftovers after the delay, sets the guardian, and rescues tokens under the rescue rules. Ownership transfer is two-step.
renounceOwnershipis disabled.Can call
setMerkleRootto set the Merkle root while unlocked.Can call
lockRootto lock the root and recordtotalAllocatedonce the contract is funded (one-way).Can call
setTgeto set or move the TGE timestamp before TGE has started, withinMAX_TGE_DELAY.Can call
setGuardianto set or clear the guardian address.Can call
setFrozento freeze or unfreeze an account.Can call
sweepUnclaimedto transfer the remaining token balance to the immutable treasury after vesting end plusSWEEP_DELAY. The call reverts whilefrozenAccountsis not zero.Can call
rescueERC20to transfer tokens to the immutable treasury (vested token only beforelockRoot).Can call
transferOwnershipto nominate a new owner (pending owner must accept).Can call
renounceOwnership, which always reverts withRenounceDisabled.
guardian: May freeze accounts. Cannot unfreeze.
Can call
setFrozenwithisFrozenset to true to freeze an account.
pendingOwner (inherited from Ownable2Step): Completes a two-step ownership transfer.
Can call
acceptOwnershipto accept ownership.
ProjectToken.sol
No privileged roles are defined.
Potential Risks
Single Points of Failure and Control: The project is fully or partially centralized, introducing single points of failure and control. This centralization can lead to vulnerabilities in decision-making and operational processes, making the system more susceptible to targeted attacks or manipulation.
Absence of Time-lock Mechanisms for Critical Operations: Without time-locks on critical operations, there is no buffer to review or revert potentially harmful actions, increasing the risk of rapid exploitation and irreversible changes.
Dependency on External Logic: MerkleVestingClaim claim and lockRoot depend on a merkleRoot and leaf allocations produced off-chain from TokenSale Purchase and Referral events. Compromise, incorrect generation, or incomplete indexing of that external logic can yield wrong allocations or an unusable claim root.
System Reliance on External Contracts: Purchases cannot complete unless the configured currency contracts honor transfers to the immutable treasury in _buy. TokenSale does not custody sale proceeds or mint ProjectToken. Distribution depends on later funding of MerkleVestingClaim with the sale token. Failure or non-standard behavior of an enabled stablecoin, or failure to fund the claim contract, blocks the raise or the claim path.
Guardian Limited to Pause and Freeze: On TokenSale, the guardian may call pause but cannot unpause, finalize, or change sale parameters. On MerkleVestingClaim, the guardian may call setFrozen only with isFrozen set to true. Unfreeze requires the owner. Prolonged pause or freeze can halt buying or block claims for frozen accounts until the owner acts.
Single Hardcoded Privileged Address: Both TokenSale and MerkleVestingClaim set treasury as an immutable constructor argument with no setter. All contribution transfers in _buy and all rescue and sweepUnclaimed proceeds go to that address. A wrong or compromised treasury chosen at deployment cannot be rotated without redeploying the affected contracts.
Operational Path for Distribution and Claim Liveness: TokenSale only accounts purchases and referrals and emits Purchase and Referral. Token delivery requires an off-chain allocation build, funding of MerkleVestingClaim with ProjectToken, and owner calls to setMerkleRoot, lockRoot, and setTge. Buying stops while TokenSale is paused, and claim reverts for a frozen account or before rootLocked and tgeTimestamp are set and reached. Incomplete event indexing, delayed root lock or TGE, extended pause, or an unresolved freeze leaves buyers and referrers with recorded entitlements and no on-chain claim path.
Merkle Allocation Trust Boundary: MerkleVestingClaim verifies leaves with MerkleProof.verifyCalldata against merkleRoot using the OpenZeppelin StandardMerkleTree double-hash of (account, totalAllocation), but does not reconstruct allocations from TokenSale state on-chain. lockRoot records owner-supplied expectedTotal after a balance check and does not prove that value equals the sum of all leaves. An incorrect root or expectedTotal that still passes funding can permanently define claimable amounts once rootLocked is true.
Guardian May be The Zero Address: TokenSale and MerkleVestingClaim accept address(0) as guardian in the constructor and in setGuardian. This is allowed by design. TokenSale treats a zero guardian as the configuration in which only the owner can pause, and MerkleVestingClaim applies the same pattern: ZeroAddress is checked for the token and treasury, not for the guardian. With a zero guardian, pause on TokenSale and freeze via setFrozen on MerkleVestingClaim are available only to the owner, so the independent emergency pause and freeze path is absent until the owner sets a non-zero guardian.
Nominal Credit Overstates The Token Obligation: In TokenSale, setCurrency stores an enabled flag and the value returned by decimals, and it reverts only when that value is above 18. The function states that fee-on-transfer and rebasing tokens must not be enabled, but the body never measures a transfer. _buy quotes _toUsd of the nominal amount, writes that figure to contributions, tokensPurchased, totalRaisedUsd, and totalTokensSold, then safeTransferFroms amount to treasury without reading the treasury balance. When the enabled token retains a fee, treasury receives less than amount while Purchase still carries the full quote, and the off-chain merkle allocation built from that event can assign more vested tokens than the stables that arrived.
Arbitrary Guardian Freeze Blocks The Full Leftover Sweep: In MerkleVestingClaim, setFrozen lets the guardian freeze any address, with no check that the address is a merkle leaf. Each transition into the frozen state increments frozenAccounts, and sweepUnclaimed reverts FrozenAccountsRemain until that count is zero. One freeze of an address that holds no allocation therefore stops the owner from sending the entire vested-token balance to treasury after sweepAllowedAt, including surplus and unclaimed tokens of accounts that are not frozen. Claims for accounts that are not frozen still succeed, and the tokens remain in the claim contract until the owner unfreezes every flagged address.
Findings
Code ― | Title | Status | Severity | |
|---|---|---|---|---|
| F-2026-1947 | Inconsistent Arguments in setCaps Leave Every Purchase Reverting While the Sale Stays Live | fixed | Low | |
| F-2026-1947 | Public Function Not Used Internally Can be Marked External | fixed | Observation |
Appendix 1. Definitions
Severities
When auditing smart contracts, Hacken is using a risk-based approach that considers Likelihood, Impact, Exploitability and Complexity metrics to evaluate findings and score severities.
Reference on how risk scoring is done is available through the repository in our Github organization:
Severity | Description |
|---|---|
Critical | Critical vulnerabilities are usually straightforward to exploit and can lead to the loss of user funds or contract state manipulation. |
High | High vulnerabilities are usually harder to exploit, requiring specific conditions, or have a more limited scope, but can still lead to the loss of user funds or contract state manipulation. |
Medium | Medium vulnerabilities are usually limited to state manipulations and, in most cases, cannot lead to asset loss. Contradictions and requirements violations. Major deviations from best practices are also in this category. |
Low | Major deviations from best practices or major Gas inefficiency. These issues will not have a significant impact on code execution. |
Severity
- Critical
Description
- Critical vulnerabilities are usually straightforward to exploit and can lead to the loss of user funds or contract state manipulation.
Severity
- High
Description
- High vulnerabilities are usually harder to exploit, requiring specific conditions, or have a more limited scope, but can still lead to the loss of user funds or contract state manipulation.
Severity
- Medium
Description
- Medium vulnerabilities are usually limited to state manipulations and, in most cases, cannot lead to asset loss. Contradictions and requirements violations. Major deviations from best practices are also in this category.
Severity
- Low
Description
- Major deviations from best practices or major Gas inefficiency. These issues will not have a significant impact on code execution.
Potential Risks
The "Potential Risks" section identifies issues that are not direct security vulnerabilities but could still affect the project’s performance, reliability, or user trust. These risks arise from design choices, architectural decisions, or operational practices that, while not immediately exploitable, may lead to problems under certain conditions. Additionally, potential risks can impact the quality of the audit itself, as they may involve external factors or components beyond the scope of the audit, leading to incomplete assessments or oversight of key areas. This section aims to provide a broader perspective on factors that could affect the project's long-term security, functionality, and the comprehensiveness of the audit findings.
Appendix 2. Scope
The scope of the project includes the following smart contracts from the provided repository:
Scope Details | |
|---|---|
| Repository | https://github.com/evacodes-dev/justify-sale→ |
| Commit | f6de3cfd55149ab608b048e7b3a6a0a5298b0936 |
| Final Commit | ca1a19cd19dca7ce912b344e4681ae0df7a4e055 |
| Whitepaper | - |
| Requirements | Readme.md; NatSpec |
| Technical Requirements | Readme.md; NatSpec |
Scope Details
- Commit
- f6de3cfd55149ab608b048e7b3a6a0a5298b0936
- Final Commit
- ca1a19cd19dca7ce912b344e4681ae0df7a4e055
- Whitepaper
- -
- Requirements
- Readme.md; NatSpec
- Technical Requirements
- Readme.md; NatSpec
Assets in Scope
Appendix 3. Additional Valuables
Additional Recommendations
The smart contracts in the scope of this audit could benefit from the introduction of automatic emergency actions for critical activities, such as unauthorized operations like ownership changes or proxy upgrades, as well as unexpected fund manipulations, including large withdrawals or minting events. Adding such mechanisms would enable the protocol to react automatically to unusual activity, ensuring that the contract remains secure and functions as intended.
To improve functionality, these emergency actions could be designed to trigger under specific conditions, such as:
Detecting changes to ownership or critical permissions.
Monitoring large or unexpected transactions and minting events.
Pausing operations when irregularities are identified.
These enhancements would provide an added layer of security, making the contract more robust and better equipped to handle unexpected situations while maintaining smooth operations.
Frameworks and Methodologies
This security assessment was conducted in alignment with recognised penetration testing standards, methodologies and guidelines, including the NIST SP 800-115 – Technical Guide to Information Security Testing and Assessment →, and the Penetration Testing Execution Standard (PTES) →, These assets provide a structured foundation for planning, executing, and documenting technical evaluations such as vulnerability assessments, exploitation activities, and security code reviews. Hacken’s internal penetration testing methodology extends these principles to Web2 and Web3 environments to ensure consistency, repeatability, and verifiable outcomes.