Q2 2026 Security & Compliance Report67 incidents, $764M in losses, 88% from operational failures.
Get the report →

Audit name:

[SCA] Justify | TokenSale | Sep2026

Date:

Sep 28, 2026

Table of Content

→Introduction
→Audit Summary
→System Overview
→Potential Risks
→Findings
→Appendix 1. Definitions
→Appendix 2. Scope
→Appendix 3. Additional Valuables
→Disclaimer

Want a comprehensive audit report like this?

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

NameSmart Contract Code Review and Security Analysis Report for Justify
Audited ByKhrystyna Tkachuk
Approved ByOlesia Bilenka
Website-
Changelog23/09/2026 - Preliminary Report
28/09/2026 - Final Report
PlatformBase
LanguageSolidity
TagsERC20; Permit Token; Vesting; Token Sales; Centralization
Methodologyhttps://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

Repositoryhttps://github.com/evacodes-dev/justify-sale→
Commitf6de3cf
Final Commitca1a19c

Audit Summary

2Total Findings
2Resolved
0Accepted
0Mitigated

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, plus pause / unpause and one-way finalize.

  • MerkleVestingClaim.sol: Merkle-gated vesting distributor for ProjectToken. After lockRoot and TGE, claim pays the unlocked portion of a leaf allocation under TGE, cliff, and linear or stepped release. Supports guardian freeze, owner sweepUnclaimed after delay, and pre-lock rescue of the vested token.

  • ProjectToken.sol: Fixed-supply ERC-20 (TOTAL_SUPPLY of 1,000,000,000 tokens, 18 decimals) with ERC20Permit. The entire supply is minted to initialHolder in 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. renounceOwnership is disabled.

    • Can call setPrice to update the token price (reverts if finalized).

    • Can call setSaleWindow to set both start and end times before the sale has started (reverts if finalized or if block.timestamp >= startTime).

    • Can call setEndTime to change the sale end time (reverts if finalized).

    • Can call setCaps to update hardCapUsd, minPurchaseUsd, and maxPerWalletUsd (reverts if finalized).

    • Can call setMaxTokensForSale to update the token sale ceiling (reverts if finalized).

    • Can call setReferralBps to update the referral rate for future purchases (reverts if finalized).

    • Can call setCurrency to enable or disable an allow-listed payment token (reverts if finalized).

    • Can call setGuardian to set or clear the guardian address (available after finalize).

    • Can call pause to pause the sale.

    • Can call unpause to unpause the sale.

    • Can call finalize to permanently finalize the sale (one-way).

    • Can call rescueERC20 to transfer stray ERC20 tokens to the immutable treasury.

    • Can call rescueETH to transfer the contract ETH balance to the immutable treasury.

    • Can call transferOwnership to nominate a new owner (pending owner must accept).

    • Can call renounceOwnership, which always reverts with RenounceDisabled.

  • guardian: May pause the sale. Cannot unpause, finalize, or change parameters.

    • Can call pause to pause the sale.

  • pendingOwner (inherited from Ownable2Step): Completes a two-step ownership transfer.

    • Can call acceptOwnership to 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. renounceOwnership is disabled.

    • Can call setMerkleRoot to set the Merkle root while unlocked.

    • Can call lockRoot to lock the root and record totalAllocated once the contract is funded (one-way).

    • Can call setTge to set or move the TGE timestamp before TGE has started, within MAX_TGE_DELAY.

    • Can call setGuardian to set or clear the guardian address.

    • Can call setFrozen to freeze or unfreeze an account.

    • Can call sweepUnclaimed to transfer the remaining token balance to the immutable treasury after vesting end plus SWEEP_DELAY. The call reverts while frozenAccounts is not zero.

    • Can call rescueERC20 to transfer tokens to the immutable treasury (vested token only before lockRoot).

    • Can call transferOwnership to nominate a new owner (pending owner must accept).

    • Can call renounceOwnership, which always reverts with RenounceDisabled.

  • guardian: May freeze accounts. Cannot unfreeze.

    • Can call setFrozen with isFrozen set to true to freeze an account.

  • pendingOwner (inherited from Ownable2Step): Completes a two-step ownership transfer.

    • Can call acceptOwnership to 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

F-2026-1947Inconsistent Arguments in setCaps Leave Every Purchase Reverting While the Sale Stays Live
Status
fixed
Severity

Low
F-2026-1947Public Function Not Used Internally Can be Marked External
Status
fixed
Severity

Observation
Code
―
Title
Status
Severity
F-2026-1947Inconsistent Arguments in setCaps Leave Every Purchase Reverting While the Sale Stays Live
fixed

Low
F-2026-1947Public Function Not Used Internally Can be Marked External
fixed

Observation
1-2 of 2 findings

Identify vulnerabilities in your smart contracts.

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

Repositoryhttps://github.com/evacodes-dev/justify-sale→
Commitf6de3cfd55149ab608b048e7b3a6a0a5298b0936
Final Commitca1a19cd19dca7ce912b344e4681ae0df7a4e055
Whitepaper-
RequirementsReadme.md; NatSpec
Technical RequirementsReadme.md; NatSpec
  • Scope Details

    Commit
    f6de3cfd55149ab608b048e7b3a6a0a5298b0936
    Final Commit
    ca1a19cd19dca7ce912b344e4681ae0df7a4e055
    Whitepaper
    -
    Requirements
    Readme.md; NatSpec
    Technical Requirements
    Readme.md; NatSpec

Assets in Scope

contracts
src
MerkleVestingClaim.sol - contracts › src › MerkleVestingClaim.sol
ProjectToken.sol - contracts › src › ProjectToken.sol
TokenSale.sol - contracts › src › TokenSale.sol

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.

Disclaimer