Q1 2026 Security & Compliance Report44 incidents, $482M in losses, insights from 11 industry leaders.
Read the report

Audit name:

[SCA] Tokenize.it | Token | Apr2026

Date:

May 17, 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 Tokenize.it team for the collaborative engagement that enabled the execution of this Smart Contract Security Assessment.

The tokenize.it protocol is a modular smart contract system for tokenizing real-world company equity on the EVM. It provides a complete lifecycle — from token issuance with compliance enforcement, through primary and secondary market sales, vesting, dividend distribution, and exit — with platform-level fee collection and an AllowList\-based KYC/attribute registry governing who may transact.

Document

NameSmart Contract Code Review and Security Analysis Report for Tokenize.it
Audited ByKornel Światłowski, Khrystyna Tkachuk
Approved ByKerem Solmaz
Websitehttps://tokenize.it/en
Changelog01/05/2026 - Preliminary Report
15/05/2026 - Final Report
10/07/2026 - Updated Final Report
PlatformEthereum
LanguageSolidity
TagsFungible Token, Permit Token, Meta Transactions, Vesting, Marketplace, Vault, Claims
Methodologyhttps://docs.hacken.io/methodologies/smart-contracts
  • Document

    Name
    Smart Contract Code Review and Security Analysis Report for Tokenize.it
    Audited By
    Kornel Światłowski, Khrystyna Tkachuk
    Approved By
    Kerem Solmaz
    Changelog
    01/05/2026 - Preliminary Report
    15/05/2026 - Final Report
    10/07/2026 - Updated Final Report
    Platform
    Ethereum
    Language
    Solidity
    Tags
    Fungible Token, Permit Token, Meta Transactions, Vesting, Marketplace, Vault, Claims

Review Scope

Repositoryhttps://github.com/corpus-io/tokenize.it-smart-contracts.git
Commit4414c631a341dfed79611d040ae0f5aa9801243f
Final Commit52b0322fb566c7143d09c23b7bd30f2e092e0691
Updated Final Commitd19dd474c0c131346174cd9aa4dfd9b36280585a

Audit Summary

23Total Findings
22Resolved
1Accepted
0Mitigated

The system users should acknowledge all the risks summed up in the risks section of the report

Documentation quality

  • Functional requirements are well documented.

    • The project overview is detailed.

    • All roles within the system are clearly described.

    • Use cases are documented and explained in detail.

    • All features of each contract are described.

    • Interactions between contracts are clearly outlined.

  • The technical description is comprehensive.

    • Run instructions are provided.

    • NatSpec documentation is sufficient.

    • A technical specification is provided.

Code quality

  • The development environment is configured.

  • The codebase reuses multiple OpenZeppelin contracts and libraries.

Test coverage

Code coverage of the project is 100% (branch coverage).

  • Deployment and basic user interactions are covered with tests.

  • Negative cases coverage is present.

System Overview

The tokenize.it protocol is a modular smart contract system for tokenizing real-world company equity on the EVM. It provides a complete lifecycle — from token issuance with compliance enforcement, through primary and secondary market sales, vesting, dividend distribution, and exit — with platform-level fee collection and an AllowList\-based KYC/attribute registry governing who may transact. It contains following contracts:

Crowdinvesting is a public token sale contract that allows anyone to purchase tokens at a configurable price, with one instance deployed per token. It supports both a static base price and dynamic pricing via an external IPriceDynamic oracle bounded by a price floor and ceiling. Tokens are either minted directly or transferred from a designated tokenHolder. The contract enforces per-receiver minimum and maximum purchase amounts, supports ERC677 (transferAndCall) in addition to standard ERC20 buy flow, and provides an optional auto-pause via a lastBuyDate timestamp. All parameter mutations require the contract to be paused and trigger a 1-hour cooldown before unpausing is allowed. The contract is deployed via clone/proxy pattern, uses Ownable2Step, Pausable, ReentrancyGuard, and supports ERC2771 meta-transactions. Fees are collected on every purchase through the token's FeeSettings contract, and the payment currency must be a TRUSTED_CURRENCY on the token's AllowList.

FeeSettings is the platform-level fee management contract for tokenize.it. Fee types (e.g., TOKEN, CROWDINVESTING, PRIVATEOFFER, SECONDARYMARKET, DISTRIBUTION, EXIT) are dynamically registered, each with a hard cap (maxNumerator), a default rate, and a default fee collector, with all numerators expressed out of a FEE_DENOMINATOR of 10,000. The owner can propose and execute default fee changes, with a 12-week delay enforced on any fee increase. Managers — a delegated role granted by the owner — can set per-token custom fee discounts (which can only reduce, never increase, the effective fee) and per-token custom fee collectors, each with an expiration date after which they revert to defaults. The contract implements IFeeSettingsV1, IFeeSettingsV2, and IFeeSettingsV3 for backward compatibility, is deployed via clone/proxy pattern, and uses Ownable2Step, ERC165, and ERC2771 meta-transactions.

Vesting handles linear ERC20 token vesting for multiple beneficiaries, supporting two token custody models: the contract holds tokens directly (transferable), or tokens are minted on release (mintable). Vesting plans are created by managers and feature a configurable allocation, cliff period, and total duration — if the cliff exceeds the duration, the duration is automatically extended to match. Plans can be created transparently (public) or privately via a commit/reveal scheme where the hash is stored first and parameters revealed later; commitments can be revoked before reveal, which limits the vesting end date. Active vesting plans can be stopped (early terminated) or paused (stopped and replaced with a new plan with adjusted terms). The beneficiary claims vested tokens via the release() function and can transfer their beneficiary status to a new address; the owner can also change the beneficiary but only one year after the vesting plan ends. The contract uses auto-incrementing uint64 plan IDs, is deployed via clone/proxy pattern, and uses Ownable2StepUpgradeable, ReentrancyGuardUpgradeable, and ERC2771 meta-transactions. The owner is automatically a manager at initialization.

Token is a UUPS-upgradeable ERC20 implementing company tokenization for the tokenize.it platform. It extends the standard ERC20 with pausing, ERC20Snapshot for historical balance queries, EIP-2612 permit, and role-based access control with eight dedicated roles: DEFAULT_ADMIN_ROLE, REQUIREMENT_ROLE, MINTALLOWER_ROLE, BURNER_ROLE, TRANSFERERADMIN_ROLE, TRANSFERER_ROLE, PAUSER_ROLE, and SNAPSHOTCREATOR_ROLE. Minting is controlled by per-address minting allowances managed by the MINTALLOWER_ROLE, with automatic fee collection to the fee collector on every mint. The BURNER_ROLE can burn tokens from any address. Every transfer (excluding mints and burns) enforces that both sender and receiver satisfy a configurable requirements bitmask checked against the AllowList, unless either party holds the TRANSFERER_ROLE. Fee settings can be migrated through a two-step suggest/accept flow requiring both the current FeeSettings owner and the token's DEFAULT_ADMIN_ROLE. The contract uses a storage gap of 446 slots to anchor the allowList variable at slot 1000, supports ERC2771 meta-transactions, and uses 18 decimals.

CoinvestedPosition holds tokens and sells them at a preset price, distributing proceeds between a co-investor (owner) and lead investors whose carry shares are each set by a profitFraction (uint32). Lead investors receive carry only on profit above the base price; the co-investor receives the residual — base portion plus leftover profit on the upside, and the full proceeds (absorbing the entire downside) whenever proceeds do not exceed the base price. The contract extends TokenSwapBase and supports buying tokens via a buy() function, claiming dividends from Distribution contracts (where the full received amount is treated as profit and split accordingly), and claiming exit proceeds from Exit contracts with carry calculated on the surplus above an effective base price. The owner can change the payment currency and base price via setCurrency(), but both this function and unpause() are blocked until a lockedUntil timestamp has elapsed. The contract integrates with GlobalTokenExitRegistry for exit claiming, starts paused on initialization, and is deployed via clone/proxy pattern with Ownable2StepUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, and ERC2771 meta-transactions.

Distribution distributes proceeds such as dividends to token holders proportional to their balances at a specific ERC20Snapshot, using a snapshot ID from Token.createSnapshot() to determine each holder's share and a fixed pricePerToken to compute the currency payout. Holders claim their share via the claim() function, with fees deducted through IFeeSettingsV3. The owner can reassign unclaimed funds from one address to another after the lockedUntil timestamp (useful for holders who lost access to their address), and can drain remaining ERC20 balances after the same lock period. Initial reassignments can be provided at initialization, bypassing the time restriction. The contract extends PayoutBase, is deployed via clone/proxy pattern, and uses Ownable2Step, ReentrancyGuard, and ERC2771 meta-transactions.

Exit is an automated exit contract where token holders exchange their full token balance for currency at a fixed pricePerToken. Holders call claim(), transferring their tokens to the contract and receiving the currency payout minus fees (deducted via IFeeSettingsV3). The received tokens are held in the contract and can be burned or extracted by the owner after the lockedUntil timestamp via drain(). The contract stores referenceToExitRate mappings for cross-currency base price conversion, used by CoinvestedPosition to calculate carry when the exit currency differs from the position's base currency. The contract is funded at initialization via safeTransferFrom from a designated currency provider, extends PayoutBase, and uses Ownable2Step, ReentrancyGuard, and ERC2771 meta-transactions.

PriceLinear is a dynamic pricing oracle implementing a linear function — either rising or falling — based on elapsed time or block count, used by Crowdinvesting as an IPriceDynamic oracle to adjust the token price over time. It computes the current price by applying a rational slope (slopeEnumerator/slopeDenominator) to a base price, with an optional step function via stepDuration that produces discrete price changes rather than a continuous line. The owner can update all parameters (slope, start time or block number, step duration, direction) via updateParameters(), which triggers a 1-hour cooldown during which getPrice() reverts. The contract is deployed via clone/proxy pattern and uses Ownable2Step and ERC2771 meta-transactions.

TokenSwapBase is the abstract base contract for token swap variants (TokenSwap and CoinvestedPosition), providing shared state for token price, currency, token, and receiver, along with initialization logic, fee handling via IFeeSettingsV3 with an IFeeSettingsV2 fallback, and pause controls. The owner can set the token price via setTokenPrice() at any time without requiring a pause or cooldown. Fee calculation uses FeeTypes.SECONDARY_MARKET when the fee settings support V3, and falls back to privateOfferFee() for V2-only deployments. The currency must be a TRUSTED_CURRENCY on the token's AllowList. The contract uses Ownable2StepUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, and ERC2771 meta-transactions.

PayoutBase is the abstract base contract for payout variants (Distribution, Exit), providing shared state for token, currency, pricePerToken, and lockedUntil, along with initialization logic, fee handling via IFeeSettingsV3 (with a silent zero-fee fallback when V3 is not supported), and a drain() recovery function. The owner can drain any ERC-20 token held by the contract via drain() once lockedUntil has elapsed. Fee calculation delegates to IFeeSettingsV3.fee() using a caller-specified fee type; if V3 is not supported the fee defaults to zero. Child contracts must implement eligible() and claim(). The contract uses Ownable2StepUpgradeable, ReentrancyGuardUpgradeable, and ERC2771ContextUpgradeable meta-transactions.

TimeLock is a time-locked ERC20 token holder that blocks all withdrawals until a specified lockedUntil timestamp, which must be in the future at initialization. The owner can claim dividends from a Distribution contract via claimDistribution() and exit proceeds from an Exit contract (via GlobalTokenExitRegistry) via claimExit() before the lock expires, forwarding the received currency to a specified recipient. After the lock expires, the owner can drain() the full balance of any ERC20 token to a recipient. There is no pause mechanism — the lock is purely time-based. The contract is deployed via clone/proxy pattern and uses Ownable2StepUpgradeable, ReentrancyGuardUpgradeable, and ERC2771 meta-transactions.

PrivateOffer is a one-time, single-buyer token purchase contract where the entire deal executes atomically in the constructor: parameters are validated, the platform fee is collected via IFeeSettingsV2.privateOfferFee(), the currency is transferred from the buyer's payer address to the company's receiver, and tokens are either minted to the buyer or transferred from a designated tokenHolder. The contract is deployed via CREATE2 for deterministic addressing, enabling privacy-preserving flows where allowances can be pre-approved to the computed address before deployment. After construction, the contract holds no state and serves no further purpose. An expiration timestamp is enforced at deployment, and the payment currency must be a TRUSTED_CURRENCY on the token's AllowList. There are no privileged roles.

AllowList is a bitmask-based attribute registry where each address is mapped to a uint256 value encoding attested attributes such as KYC status, citizenship, and tier levels, with 252 attribute bits, 4 cumulative tier bits, and the highest bit (bit 255) reserved for the TRUSTED_CURRENCY flag. The owner can set or update attributes for individual addresses or in batch via set(), and remove addresses (resetting their attributes to zero) via remove(). The contract is used by Token to enforce transfer compliance requirements and by all sale contracts, to verify trusted currencies. It is deployed via clone/proxy pattern, uses Ownable2Step and ERC2771 meta-transactions, and a single AllowList instance can serve an unlimited number of Token contracts.

TokenSwap is a two-way token swap contract supporting both buy and sell orders at a fixed price, extending TokenSwapBase. It uses an external holder address for liquidity: buyers pay currency to the receiver and receive tokens transferred from the holder, while sellers send tokens to the receiver and receive currency transferred from the holder. The order size is implicitly capped by the holder's allowance and balance. Fees are collected via FeeTypes.SECONDARY_MARKET on both buy() and sell() operations. buy() rounds the currency amount up (ceil) while sell() rounds it down (floor). The contract is deployed via clone/proxy pattern and inherits Ownable2StepUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, and ERC2771 meta-transactions from TokenSwapBase.

GlobalTokenExitRegistry is a singleton registry that stores the authorized Exit contract for each Token. Once an exit is set for a token via setExit(), the binding is immutable and cannot be changed or removed. Authorization is derived from the target token's access control: the caller must hold DEFAULT_ADMIN_ROLE on the token or be its owner(), with both models checked via try/catch to accommodate different access control implementations. The registry is used by CoinvestedPosition and TimeLock to locate the exit contract for a given token. It is not upgradeable, supports ERC2771 meta-transactions, and has no admin roles of its own.

Privileged roles

Crowdinvesting.sol

  • Owner (Ownable2Step):

    • activateDynamicPricing() — enables a price oracle to influence the token price (whenPaused).

    • deactivateDynamicPricing() — deactivates dynamic pricing, reverting to base price (whenPaused).

    • setCurrencyReceiver() — changes the address that receives currency payments (whenPaused).

    • setMinAmountPerReceiver() — changes the minimum token amount per receiver (whenPaused).

    • setMaxAmountPerReceiver() — changes the maximum token amount per receiver (whenPaused).

    • setCurrencyAndTokenPrice() — changes the payment currency and token price; deactivates dynamic pricing (whenPaused).

    • setMaxAmountOfTokenToBeSold() — changes the total token supply cap for the sale (whenPaused).

    • setLastBuyDate() — sets the auto-pause date after which no purchases are accepted (whenPaused).

    • setTokenHolder() — changes the address from which tokens are transferred (whenPaused).

    • pause() — pauses the contract, preventing all purchases.

    • unpause() — unpauses the contract (requires cooldown to have elapsed).

FeeSettings.sol

  • Owner (Ownable2Step):

    • addManager() — grants manager status to an address.

    • removeManager() — revokes manager status from an address.

    • registerFeeType() — registers a new fee type with hard cap, default rate, and collector.

    • planFeeChange() — proposes a new default fee numerator for a fee type (12-week delay for increases).

    • executeFeeChange() — executes a previously planned default fee change after activation date.

    • setDefaultFeeCollector() — sets the default fee collector address for a fee type.

  • Manager (custom onlyManager modifier; owner is automatically a manager at initialization):

    • setCustomFee() — sets a per-token custom fee discount with a validity date.

    • removeCustomFee() — removes a per-token custom fee discount, reverting to default.

    • setCustomFeeCollector() — sets a per-token fee collector override for a fee type.

    • removeCustomFeeCollector() — removes a per-token fee collector override, reverting to default.

Vesting.sol

  • Owner (Ownable2StepUpgradeable; owner is automatically a manager at initialization):

    • addManager() — grants manager status to an address.

    • removeManager() — revokes manager status from an address.

    • changeBeneficiary() — changes the beneficiary of a vesting plan (only 1 year after vesting end).

  • Manager (custom onlyManager modifier):

    • commit() — commits to a private vesting plan by storing its hash.

    • revoke() — revokes a commitment by setting a new latest end date.

    • createVesting() — creates a public vesting plan with allocation, beneficiary, cliff, and duration.

    • stopVesting() — stops (early terminates) an active vesting plan.

    • pauseVesting() — pauses a vesting plan by stopping it and creating a new one with adjusted terms.

  • Beneficiary (per vesting plan):

    • release() — releases vested tokens to the beneficiary.

    • changeBeneficiary() — changes the beneficiary to a new address.

Token.sol

  • DEFAULT_ADMIN_ROLE:

    • _authorizeUpgrade() — authorizes a UUPS proxy upgrade to a new implementation.

    • setAllowList() — changes the AllowList contract used for compliance enforcement.

    • acceptNewFeeSettings() — accepts a previously suggested new FeeSettings contract.

    • All role management — can grant/revoke all roles except TRANSFERER_ROLE (managed by TRANSFERERADMIN_ROLE).

  • REQUIREMENT_ROLE:

    • setRequirements() — changes the transfer compliance requirements bitmask.

  • MINTALLOWER_ROLE:

    • increaseMintingAllowance() — increases the minting allowance for a specific address.

    • decreaseMintingAllowance() — decreases the minting allowance for a specific address.

    • mint() — can mint unlimited tokens (bypasses minting allowance check).

  • BURNER_ROLE:

    • burn() — burns tokens from any address.

  • TRANSFERERADMIN_ROLE:

    • Role admin for TRANSFERER_ROLE — can grant/revoke TRANSFERER_ROLE to/from any address.

  • TRANSFERER_ROLE:

    • Holders bypass all compliance requirement checks when sending or receiving tokens.

  • PAUSER_ROLE:

    • pause() — pauses the token; no transfers, minting, or burning possible.

    • unpause() — unpauses the token.

  • SNAPSHOTCREATOR_ROLE:

    • createSnapshot() — creates a snapshot of all current token balances.

  • FeeSettings Owner (external):

    • suggestNewFeeSettings() — suggests a new FeeSettings contract (requires DEFAULT_ADMIN_ROLE acceptance).

CoinvestedPosition.sol

  • Owner (Ownable2StepUpgradeable, inherited from TokenSwapBase):

    • unpause() — unpauses the contract (requires tokenPrice != 0 and lockedUntil elapsed).

    • setCurrency() — changes the payment currency, base price, and token price(requires lockedUntil elapsed).

    • claimDistribution() — claims dividends from a Distribution contract and splits proceeds.

    • claimExit() — claims exit proceeds and splits between receiver and lead investors.

    • setTokenPrice() — sets the token sale price (inherited from TokenSwapBase).

    • pause() — pauses the contract (inherited from TokenSwapBase).

    • withdrawAsCoinvestor() — withdraw the coinvestor's accumulated credit in specific _currency to specified _receiver.

    • ownerRotateLeadInvestorAccount() - rotate the address of a lead-investor slot.

  • Lead Investor (custom role defined during contract initialziation)

    • rotateLeadInvestorAccount() - rotate the address that controls a given lead-investor slot.

    • withdrawAsLeadInvestor() - withdraw a lead investor's accumulated credit in _currency to _receiver.

Distribution.sol

  • Owner (Ownable2Step, inherited from PayoutBase):

    • reassign() — reassigns unclaimed distribution funds from one address to another (after lockedUntil).

    • drain() — transfers the entire balance of any ERC20 held by this contract to a recipient (after lockedUntil).

Exit.sol

  • Owner (Ownable2Step, inherited from PayoutBase):

    • drain() — transfers the entire balance of any ERC20 held by this contract to a recipient (after lockedUntil).

PriceLinear.sol

  • Owner (Ownable2Step):

    • updateParameters() — updates the linear price function parameters (slope, start, step duration, direction).

TokenSwapBase.sol (abstract)

  • Owner (Ownable2StepUpgradeable):

    • setTokenPrice() — sets the token sale/buy price.

    • pause() — pauses the contract.

    • unpause() — abstract, implemented by children.

PayoutBase.sol (abstract)

  • Owner (Ownable2Step):

    • drain() — transfers the full balance of any ERC-20 token held by the contract to a chosen recipient, only after lockedUntil has passed.

TimeLock.sol

  • Owner (Ownable2StepUpgradeable):

    • claimDistribution() — claims dividends from a Distribution contract and forwards to a recipient.

    • claimExit() — claims exit proceeds for locked tokens and forwards to a recipient.

    • drain() — transfers the full balance of any ERC20 to a recipient (only after lockedUntil).

PrivateOffer.sol

No privileged roles. All logic executes atomically in the constructor. No persistent state or admin functions exist after deployment.

AllowList.sol

  • Owner (Ownable2Step):

    • set(address, uint256) — sets or updates the attribute bitmask for a single address.

    • set(address[], uint256[]) — batch sets attributes for multiple addresses.

    • remove(address) — removes a single address from the allow list.

    • remove(address[]) — batch removes multiple addresses from the allow list.

TokenSwap.sol

  • Owner (Ownable2StepUpgradeable, inherited from TokenSwapBase):

    • unpause() — unpauses the contract.

    • setTokenPrice() — sets the token price (inherited from TokenSwapBase).

    • pause() — pauses the contract (inherited from TokenSwapBase).

GlobalTokenExitRegistry.sol

  • Token DEFAULT_ADMIN_ROLE Holder or Token Owner (authorization derived from the target Token contract, not from the registry itself):

    • setExit() — registers an Exit contract for a Token (one-time, immutable binding). Caller must hold DEFAULT_ADMIN_ROLE on the token or be the token's owner().

Potential Risks

Centralized Control Over Transfer Restrictions and Allowlist Logic: The token contract enforces requirement checks on every transfer, relying on user attributes stored in an external allowlist contract. The contract owner can arbitrarily modify user attributes, while privileged roles can update both the transfer requirements and the allowlist contract address. This design introduces a centralization risk, where changes to requirements, user attributes, or the allowlist implementation could block transfers for specific users or the entire system. Additionally, if DeFi protocols are not properly designated as privileged, user interactions such as fund recovery or token swaps may be restricted, potentially leading to loss of access to funds.

Owner-Controlled Base Price in CoinvestedPosition.claimExit Enables Carry Manipulation: When the exit currency differs from the stored investment currency and no referenceToExitRate is configured, claimExit() falls back to an owner-supplied _basePrice parameter with no on-chain validation against market rates. The owner can exploit this by inflating _basePrice so that basePayout equals or exceeds the total received amount, effectively zeroing the carry owed to lead investors. This creates a trust dependency where a malicious or compromised owner can silently steal carry from lead investors without any safeguard or independent price verification.

GlobalTokenExitRegistry Exit Assignment Is Permanent and Irreversible: The setExit() function in GlobalTokenExitRegistry uses an ExitAlreadySet check that prevents any update once an exit contract is assigned to a token. There is no emergency override, governance mechanism, or admin function to reassign the exit contract. If the assigned exit contract contains a bug, becomes compromised, or needs to be upgraded, the registry offers no recovery path, permanently locking the token to the faulty exit contract.

FeeSettings Manager Can Redirect Fee Collection Without Owner Approval: The setCustomFeeCollector() function in FeeSettings is gated by onlyManager rather than onlyOwner, and has no timelock or owner approval requirement. A compromised or malicious manager can redirect all platform fee revenue for any token to an attacker-controlled address. This represents a significant trust assumption, as the manager role alone has full authority over fee collection destinations without additional oversight.

Scope Definition and Security Guarantees: The audit does not cover all code in the repository. Contracts outside the audit scope may introduce vulnerabilities, potentially impacting the overall security due to the interconnected nature of smart contracts.

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.

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.

Owner's Unrestricted State Modification: The absence of restrictions on state variable modifications by the owner leads to arbitrary changes, affecting contract integrity and user trust, especially during critical operations like minting phases.

Administrative Key Control Risks: The digital contract architecture relies on administrative keys for critical operations. Centralized control over these keys presents a significant security risk, as compromise or misuse can lead to unauthorized actions or loss of funds.

Owner-Controlled Price Setting Across Contracts: The token price used for buying, selling, and exit settlements is directly provided by the owner without independent price discovery or external verification. In TokenSwapBase.setTokenPrice(), the owner may change the price at any time without pause or cooldown constraints. In Crowdinvesting, the owner selects the oracle address and sets both the base price and the min/max bounds that constrain oracle output. In CoinvestedPosition.claimExit(), the owner supplies a fallback base price parameter that determines the carry/receiver split when no on-chain rate exists.

Trusted Forwarder Can Impersonate Any Address Across All Platform Contracts: All core contracts inherit ERC2771ContextUpgradeable with an immutable trusted forwarder set at construction. This forwarder can submit transactions impersonating any address, enabling it to execute transfers, approvals, minting, and role-gated functions on behalf of any user. Since the forwarder cannot be rotated or revoked post-deployment, a compromised forwarder represents a single point of failure capable of draining tokens and bypassing access control protocol-wide.

Allowlist Requirements Can Freeze User Tokens Without Recourse: The Token contract enforces a bitmask check (allowList.map(_address) & requirements requirements) on every transfer for both sender and receiver. If a user's AllowList attributes are revoked, requirements are raised after distribution, or the AllowList contract is replaced, all transfers for affected users are blocked immediately. There is no self-service appeal, cooldown, or governance mechanism — token recovery depends entirely on privileged actors restoring attributes or granting the TRANSFERER_ROLE.

Unrestricted Burn From Any Address by BURNER_ROLE: The BURNER_ROLE in the Token contract can burn tokens from any holder without their consent. While this appears intended for legal recovery and lost-key reissuance flows, the current NatSpec only describes the mechanical burn behavior and does not document the intended use cases, operational preconditions, or governance process required before execution. If the BURNER_ROLE is compromised or maliciously assigned, an attacker could arbitrarily destroy user balances.

Owner Recovery Mechanism Requires Lead Investor Vigilance: The 90-day owner recovery mechanism is documented and appears to be an intentional trust trade-off. However, lead investors must actively monitor the recoveryArmedAt state and disarm recovery, either through withdrawal or self-rotation, to prevent the owner from completing a takeover after the recovery delay. Since the owner can repeatedly trigger small credit events, such as selling 1 token, the recovery timer may be re-armed over time, which may lead to unintended owner recovery execution if lead investors are not actively monitoring the recovery state.

Unprivileged Dust-Token Injection Can Permanently Block Exit Redemptions: Exit.claim() redeems a holder’s entire live token balance, so anyone can transfer a tiny amount of extra tokens to a TimeLock or CoinvestedPosition contract and make the required payout exceed the exit reserve. Because these holder contracts cannot freely dispose of the injected dust, their claimExit() calls can remain reverted, preventing legitimate users from redeeming their position.

Findings

Code
Title
Status
Severity
F-2026-1655Push Based Settlement With Immutable Recipients Enables Permanent Denial Of Service
fixed

Medium
F-2026-1659Inconsistent Validation of LockedUntil Enables Premature Privileged Actions
fixed

Medium
F-2026-1677Missing Zero-Address Validation for _owner in PayoutBase May Lead to Loss of Administrative Control
fixed

Low
F-2026-1677 Zero Price Allows Free Token Acquisition and Causes Division-by-Zero
fixed

Low
F-2026-1674Vesting.commit Overwrites Existing Commitment State, Including Revoked or Already-Revealed Hashes
fixed

Low
F-2026-1666Max Purchase Limit Is Applied Per Receiver Instead of Per Buyer
fixed

Low
F-2026-1664Asymmetric Parameter Update Logic Allows Immediate Price Availability
fixed

Low
F-2026-1660Lack of Exit Token Validation Allows Mismatched Contract Binding
fixed

Low
F-2026-1655Privileged Burn Function Allows Arbitrary Token Destruction
accepted

Low
F-2026-1648Currency Update Without Token Price Sync Leads To Mispricing
fixed

Low
1-10 of 23 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/corpus-io/tokenize.it-smart-contracts.git
Commit4414c631a341dfed79611d040ae0f5aa9801243f
Final Commit52b0322fb566c7143d09c23b7bd30f2e092e0691
Updated Final Commitd19dd474c0c131346174cd9aa4dfd9b36280585a
Whitepaper-
Requirementshttps://github.com/corpus-io/tokenize.it-smart-contracts/tree/4414c631a341dfed79611d040ae0f5aa9801243f/docs
Technical Requirementshttps://github.com/corpus-io/tokenize.it-smart-contracts/blob/4414c631a341dfed79611d040ae0f5aa9801243f/README.md

Assets in Scope

.
AllowList.sol - . › AllowList.sol
CoinvestedPosition.sol - . › CoinvestedPosition.sol
common
TokenSwapBase.sol - . › common › TokenSwapBase.sol
Crowdinvesting.sol - . › Crowdinvesting.sol
Distribution.sol - . › Distribution.sol
Exit.sol - . › Exit.sol
FeeSettings.sol - . › FeeSettings.sol
GlobalTokenExitRegistry.sol - . › GlobalTokenExitRegistry.sol
PriceLinear.sol - . › PriceLinear.sol
PrivateOffer.sol - . › PrivateOffer.sol
TimeLock.sol - . › TimeLock.sol
Token.sol - . › Token.sol
TokenSwap.sol - . › TokenSwap.sol
Vesting.sol - . › Vesting.sol
common
PayoutBase.sol - common › PayoutBase.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