Q3 2026 Security & Compliance$1.18B stolen across 107 incidents. Main drivers: supply chain, smart contracts, keys.
Get the report →

Audit name:

[SCA] Tortuga Estate | TRWA contracts | Jun2026

Date:

Jul 16, 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 Tortuga Estate team for the collaborative engagement that enabled the execution of this Smart Contract Security Assessment.

Tortuga TRWA is an ERC-3643 (T-REX) compliant security token platform, designed for tokenizing real-world real estate assets. The satellite token contract enforces Pattern A authorization for mint and burn operations via a dedicated authorisedSigner (an Internet Computer canister's Chain Fusion threshold ECDSA key), while forced transfers, wallet recovery, and freeze operations are delegated to the ERC-3643 agent role. A modular compliance layer enforces per-property regulatory rules such as jurisdiction restrictions, holding periods, investor categorization, holder caps, and sanctions/freeze checks.

Document

NameSmart Contract Code Review and Security Analysis Report for Tortuga Estate
Audited ByKhrystyna Tkachuk, Kornel Światłowski
Approved BySeher Saylik
Websitehttps://tortuga.estate→
Changelog06/07/2026 - Preliminary Report
31/07/2026 - Final Report
PlatformEthereum; Base; Plume
LanguageSolidity
TagsRWA; Upgradable; ERC3643
Methodologyhttps://docs.hacken.io/methodologies/smart-contracts→

Review Scope

Repositoryhttps://github.com/tortuga-estate/trwa-contracts-hacken→
Commitb657648
Final Commitf0b8e7b

Audit Summary

12Total Findings
7Resolved
5Accepted
0Mitigated

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

Documentation quality

  • Functional requirements are detailed.

    • Project overview is detailed.

    • All roles in the system are described.

    • Use cases are described and detailed.

    • All futures are described.

  • Technical description is detailed.

    • Run instructions are provided.

    • Technical specification is provided.

    • NatSpec is sufficient.

Code quality

  • Common upstream contracts are reused through dependencies, but TokenVirtualPatched.sol vendors ERC-3643 code with documented minimal changes, which adds maintenance overhead.

  • The development environment is configured.

Test coverage

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

  • Deployment, initialization, minting, burning, transfers, freezing, recovery, governance, compliance modules, and basic user interactions are covered with tests.

  • Negative cases coverage are present.

  • Invariant testing is present for supply, frozen balances, nonce usage, pause state, and authority immutability.

  • Interactions by several users are tested.

System Overview

The architecture centers on TortugaTRWA, a UUPS-upgradeable ERC-3643 token that extends a vendored copy of the stock T-REX Token.sol (TokenVirtualPatched). The vendored base adds thirteen virtual keywords to enable override hooks in the child contract. TortugaTRWA replaces the stock agent-gated mint and burn with tortugaMint and tortugaBurn, which require a dedicated authorisedSigner address (the ICP canister's Chain Fusion ECDSA key) and enforce replay defenses via per-call nonce, chain ID, property ID, and expiry validation. Any token holder can burn their tokens through holderSelfBurn to redeem their position. A two-Safe governance topology separates pause authority (a 2-of-5 emergencyPauseSafe) from broad administrative authority (the 3-of-5 owner Safe), while compliance and identity-registry hot-swaps are gated by a 24-hour on-chain timelock that can be waived under a declared SEV-1 incident. Wallet recovery, forced transfers, and freeze operations are delegated to the ERC-3643 agent role, with compliance hooks made best-effort on emergency paths so that a reverting module cannot block agent authority.

The compliance subsystem is composed of six pluggable modules, all inheriting from AbstractTortugaModule (which itself extends Tokeny's AbstractModuleUpgradeable). Each module is deployed behind its own ERC-1967 UUPS proxy and wired to a stock Tokeny ModularCompliance contract. TortugaComplianceFactory orchestrates per-property deployment by instantiating a ModularCompliance proxy, selectively deploying each enabled module proxy, initializing configuration, binding modules, and transferring ownership to the specified _owner. Three modules are stateful (MaxHolderCountModule, PropertyHoldingPeriodModule, FinmaInvestorCategoryModule) and carry per-holder state keyed by compliance address; they expose migrateFrom and migrateForRecoveredWallet hooks so state survives compliance hot-swaps and wallet recoveries. Three modules are stateless (SanctionsFreezeModule, EligibleJurisdictionModule, MaxBalancePerHolderModule), reading their enforcement data from the bound token or identity registry at call time. Stateful modules use a migrationComplete gate that fails closed until explicitly marked by the owner, preventing a freshly-bound module from silently relaxing policy.

The system implements the ERC-3643 standard (ERC-20 with identity-registry-backed transfer restrictions and modular compliance checks). Upgradeable infrastructure relies on OpenZeppelin's UUPS proxy pattern and OwnableUpgradeable for ownership. The token's upgrade authority is restricted to the owner, and all modules block renounceOwnership to prevent permanent bricking of owner-gated configuration paths.

Files in Scope

TortugaTRWA.sol — The satellite ERC-3643 security token. Implements Pattern A authorization (tortugaMint, tortugaBurn gated by authorisedSigner), replay defenses (nonce, chainId, expiry, propertyId), a two-Safe pause topology, 24-hour timelocked compliance and identity-registry hot-swap with SEV-1 waiver, holder self-burn, best-effort compliance hooks on forced transfers and recovery, auto-migration of stateful modules during wallet recovery, and UUPS upgrade gating.

MaxHolderCountModule.sol — Stateful compliance module that caps the number of unique non-zero token holders per bound compliance. Tracks holder additions and removals via moduleMintAction, moduleTransferAction, and moduleBurnAction using the bound token's real balanceOf as the source of truth. Provides migrateFrom for compliance hot-swap state carry and migrateForRecoveredWalletFromToken for token-driven recovery migration. Enforces a migrationComplete gate that fails closed until the operator confirms state readiness.

TortugaComplianceFactory.sol — Deploys a per-property compliance stack in a single transaction. Instantiates a ModularCompliance proxy and up to six module proxies (sanctions, jurisdiction, holding period, holder count, max balance, FINMA category), initializes each with its configuration, wires modules to compliance via addModule, and transfers ownership of all contracts to the caller.

PropertyHoldingPeriodModule.sol — Stateful compliance module that enforces a minimum holding duration per holder. Records firstAcquired timestamps on mint and transfer receipt, blocks outbound transfers until the holding period elapses, and clears the record on full exit (burn-to-zero). Provides migrateFrom and migrateForRecoveredWalletFromToken for state migration during compliance hot-swaps and wallet recoveries. Enforces a migrationComplete gate and caps the configurable period at MAX_HOLDING_PERIOD (3,650 days).

FinmaInvestorCategoryModule.sol — Stateful compliance module restricting transfers by FINMA investor category (Retail, Qualified, Institutional). Assigns per-holder categories keyed by compliance address and checks a configurable sender-to-recipient policy matrix (allowedPair) on each transfer. Supports batch category assignment and owner-gated migration from a prior module instance via migrateFromPriorModule. Provides migrateForRecoveredWalletFromToken for token-driven recovery. Enforces a migrationComplete gate.

EligibleJurisdictionModule.sol — Stateless compliance module that restricts transfers to holders in whitelisted countries (ISO 3166-1 numeric codes). Reads each party's country from the bound token's identity registry via investorCountry and checks against a module-side eligibleCountry allowlist. Rejects country code 0 from ever being marked eligible, ensuring un-onboarded holders cannot transact.

MaxBalancePerHolderModule.sol — Stateless compliance module that caps each individual holder's token balance. The per-holder cap is keyed by compliance address, allowing different properties to have different limits. Uses the bound token's balanceOf at check time, accounts for self-transfers as a no-op, and exposes a maxBalance reader used by TortugaTRWA to emit RecoveryExceedsMaxBalance warnings.

SanctionsFreezeModule.sol — Stateless compliance module that delegates freeze enforcement to the bound token's own freeze state. Reads isFrozen and getFrozenTokens from the token via the compliance's getTokenBound accessor and rejects transfers where either party is fully frozen or the sender lacks sufficient unfrozen balance.

AbstractTortugaModule.sol — Abstract base contract for all Tortuga compliance modules. Extends Tokeny's AbstractModuleUpgradeable with a shared initializer (__TortugaModule_init), constructor-based initializer disabling for implementation contracts, renounceOwnership blocking, plug-and-play declaration, a virtual isStateful hook (defaults to false), and UUPS upgrade authorization gated to the owner.

TokenVirtualPatched.sol — Vendored copy of the upstream ERC-3643 Token.sol (v4.1.3) with thirteen virtual keywords added to enable overrides in TortugaTRWA. Provides the core ERC-20 token logic (transfer, approve, mint, burn), ERC-3643 compliance integration (identity registry checks, modular compliance hooks), agent-role gating, pause/unpause, address and partial-token freezing, forced transfers, and wallet recovery.

Privileged roles

TortugaTRWA.sol:

  • owner (inherited from TokenVirtualPatched via AgentRoleUpgradeable / OwnableUpgradeable): The Main Admin Safe (3-of-5). Holds the broadest set of administrative powers.

    • Can call setAuthorisedSigner to rotate the canister-derived Chain Fusion ECDSA signer address (immediate, no timelock).

    • Can call setEmergencyPauseSafe to rotate the emergency pause Safe address.

    • Can call unpause to resume token operations after a pause.

    • Can call declareSev1 to waive the 24-hour timelock on the next compliance or registry swap.

    • Can call clearSev1 to revoke a previously declared SEV-1 waiver and retroactively reinstate the timelock on any in-flight pending swap.

    • Can call queueComplianceSwap to queue a new compliance contract for hot-swap with a 24-hour timelock (or immediate under SEV-1).

    • Can call cancelComplianceSwap to cancel an in-flight compliance hot-swap.

    • Can call executeComplianceSwap to finalize a timelocked compliance hot-swap.

    • Can call queueIdentityRegistrySwap to queue a new identity registry for hot-swap with a 24-hour timelock (or immediate under SEV-1).

    • Can call cancelIdentityRegistrySwap to cancel an in-flight identity registry swap.

    • Can call executeIdentityRegistrySwap to finalize a timelocked identity registry hot-swap.

    • Can call setOnchainID to update the token's OnchainID address.

    • Can call setName to update the token name (inherited from TokenVirtualPatched).

    • Can call setSymbol to update the token symbol (inherited from TokenVirtualPatched).

    • Can call transferOwnership to transfer contract ownership to a new address (inherited from OwnableUpgradeable).

    • Can call addAgent and removeAgent to manage the agent role set (inherited from AgentRoleUpgradeable).

    • UUPS upgrade authority: _authorizeUpgrade restricts implementation upgrades to the owner.

    • renounceOwnership is explicitly blocked.

  • authorisedSigner: The canister-derived Chain Fusion ECDSA address. The only address permitted to mint and burn tokens.

    • Can call tortugaMint to mint tokens to a verified address with replay-protected payload (nonce, chainId, propertyId, expiry).

    • Can call tortugaBurn to burn tokens from a holder's unfrozen balance with replay-protected payload.

  • emergencyPauseSafe: A separate Safe (2-of-5) with pause-only authority.

    • Can call pause to halt all non-emergency token operations.

  • agent (inherited from TokenVirtualPatched via AgentRoleUpgradeable): Addresses granted the agent role by the owner.

    • Can call forcedTransfer to execute emergency token transfers (court-ordered seizure, sanctions liquidation) with best-effort compliance hooks.

    • Can call recoveryAddress to recover tokens from a lost wallet to a new wallet, including auto-migration of stateful module state, identity re-registration, and freeze carry-forward.

    • Can call setAddressFrozen to freeze or unfreeze an address entirely.

    • Can call freezePartialTokens to freeze a specified amount of tokens for an address.

    • Can call unfreezePartialTokens to unfreeze a specified amount of tokens for an address.

    • Can call batchSetAddressFrozen, batchFreezePartialTokens, batchUnfreezePartialTokens, and batchForcedTransfer for batch operations (inherited from TokenVirtualPatched).

MaxHolderCountModule.sol:

  • owner (inherited from AbstractTortugaModule via AbstractModuleUpgradeable / OwnableUpgradeable): Module administrator.

    • Can call setMaxHolderCount to update the maximum number of unique holders allowed.

    • Can call markMigrationComplete to unlock rule enforcement for a specific compliance binding.

    • Can call transferOwnership to transfer module ownership.

    • UUPS upgrade authority: _authorizeUpgrade restricts implementation upgrades to the owner (inherited from AbstractTortugaModule).

    • renounceOwnership is explicitly blocked (inherited from AbstractTortugaModule).

  • compliance (via onlyComplianceCall modifier inherited from AbstractModuleUpgradeable): The bound compliance contract.

    • Can call moduleMintAction, moduleTransferAction, moduleBurnAction to update holder-count tracking after token state changes.

    • Can call migrateFrom to copy holder-set state from a prior module instance during compliance hot-swap.

    • Can call migrateForRecoveredWallet (deliberate no-op that emits RecoveryMigrationNoop; holder-count accuracy is maintained by moduleTransferAction via real-balance checks during the recovery's forced transfer).

  • bound token (via require(msg.sender getTokenBound()) check): The token contract bound to the compliance.

    • Can call migrateForRecoveredWalletFromToken to trigger recovery-time tracked-balance migration from within the token's recoveryAddress flow.

TortugaComplianceFactory.sol:

  • No privileged roles are retained by the factory post-deployment. The factory is a stateless deployer that transfers ownership of all created contracts (compliance and modules) to the _owner parameter during deployForProperty. Any address may call deployForProperty.

PropertyHoldingPeriodModule.sol:

  • owner (inherited from AbstractTortugaModule via AbstractModuleUpgradeable / OwnableUpgradeable): Module administrator.

    • Can call setMinHoldingPeriod to update the minimum holding period (capped at MAX_HOLDING_PERIOD of 3,650 days).

    • Can call markMigrationComplete to unlock rule enforcement for a specific compliance binding.

    • Can call transferOwnership to transfer module ownership.

    • UUPS upgrade authority: _authorizeUpgrade restricts implementation upgrades to the owner (inherited from AbstractTortugaModule).

    • renounceOwnership is explicitly blocked (inherited from AbstractTortugaModule).

  • compliance (via onlyComplianceCall): The bound compliance contract.

    • Can call moduleMintAction, moduleTransferAction, moduleBurnAction to update per-holder acquisition timestamps and tracked balances.

    • Can call migrateFrom to copy holding-period state from a prior module instance during compliance hot-swap.

    • Can call migrateForRecoveredWallet to copy the lost wallet's holding-period clock to the new wallet during recovery.

  • bound token (via require(msg.sender getTokenBound())): The token contract bound to the compliance.

    • Can call migrateForRecoveredWalletFromToken to trigger recovery-time holding-period migration from within the token's recoveryAddress flow.

FinmaInvestorCategoryModule.sol:

  • owner (inherited from AbstractTortugaModule via AbstractModuleUpgradeable / OwnableUpgradeable): Module administrator.

    • Can call setHolderCategory to assign a FINMA investor category (Retail, Qualified, Institutional) to a holder under a specific compliance.

    • Can call batchSetHolderCategory to assign categories to multiple holders in a single transaction.

    • Can call setAllowedPair to configure the sender-to-recipient category policy matrix.

    • Can call markMigrationComplete to unlock rule enforcement for a specific compliance binding.

    • Can call migrateFromPriorModule to copy holder-category assignments from a previous module instance across compliance pairs.

    • Can call migrateForRecoveredWallet to copy the lost wallet's FINMA category to the new wallet during recovery.

    • Can call transferOwnership to transfer module ownership.

    • UUPS upgrade authority: _authorizeUpgrade restricts implementation upgrades to the owner (inherited from AbstractTortugaModule).

    • renounceOwnership is explicitly blocked (inherited from AbstractTortugaModule).

  • compliance (via onlyComplianceCall): The bound compliance contract.

    • Can call moduleMintAction, moduleTransferAction, moduleBurnAction (no-op hooks for compliance callback compatibility).

  • bound token (via require(msg.sender getTokenBound())): The token contract bound to the compliance.

    • Can call migrateForRecoveredWalletFromToken to trigger recovery-time FINMA category migration from within the token's recoveryAddress flow.

EligibleJurisdictionModule.sol:

  • owner (inherited from AbstractTortugaModule via AbstractModuleUpgradeable / OwnableUpgradeable): Module administrator.

    • Can call setCountryEligibility to enable or disable a country code in the jurisdiction allowlist (country code 0 can never be enabled).

    • Can call batchSetCountryEligibility to update eligibility for multiple country codes in a single transaction.

    • Can call transferOwnership to transfer module ownership.

    • UUPS upgrade authority: _authorizeUpgrade restricts implementation upgrades to the owner (inherited from AbstractTortugaModule).

    • renounceOwnership is explicitly blocked (inherited from AbstractTortugaModule).

  • compliance (via onlyComplianceCall): The bound compliance contract.

    • Can call moduleMintAction, moduleTransferAction, moduleBurnAction (no-op hooks for compliance callback compatibility).

MaxBalancePerHolderModule.sol:

  • owner (inherited from AbstractTortugaModule via AbstractModuleUpgradeable / OwnableUpgradeable): Module administrator.

    • Can call setMaxBalance to set the per-holder token balance cap for a specific compliance binding.

    • Can call transferOwnership to transfer module ownership.

    • UUPS upgrade authority: _authorizeUpgrade restricts implementation upgrades to the owner (inherited from AbstractTortugaModule).

    • renounceOwnership is explicitly blocked (inherited from AbstractTortugaModule).

  • compliance (via onlyComplianceCall): The bound compliance contract.

    • Can call moduleMintAction, moduleTransferAction, moduleBurnAction (no-op hooks for compliance callback compatibility).

SanctionsFreezeModule.sol:

  • owner (inherited from AbstractTortugaModule via AbstractModuleUpgradeable / OwnableUpgradeable): Module administrator.

    • Can call transferOwnership to transfer module ownership.

    • UUPS upgrade authority: _authorizeUpgrade restricts implementation upgrades to the owner (inherited from AbstractTortugaModule).

    • renounceOwnership is explicitly blocked (inherited from AbstractTortugaModule).

  • compliance (via onlyComplianceCall): The bound compliance contract.

    • Can call moduleMintAction, moduleTransferAction, moduleBurnAction (no-op hooks for compliance callback compatibility).

Potential Risks

Dependency on External ERC-3643 Framework: TortugaTRWA inherits from TokenVirtualPatched (a vendored copy of Token.sol), which in turn inherits from AgentRoleUpgradeable, TokenStorage, and the full ERC-3643 framework including ModularCompliance, IIdentityRegistry, and IToken. The compliance factory (TortugaComplianceFactory) deploys proxies of Tokeny's stock ModularCompliance contract. All compliance modules interact with the IModularCompliance interface and depend on its getTokenBound, addModule, bindToken, and callback dispatch. A vulnerability in the upstream ERC-3643 contracts (not in scope) could propagate to all in-scope contracts that depend on this framework.

Dependency on OnchainID Identity System: The recoveryAddress function in TortugaTRWA calls IIdentity(_investorOnchainID).keyHasPurpose to verify the recovery key, and the identity registry's isVerified, registerIdentity, deleteIdentity, and investorCountry functions are called during mint, transfer, and recovery flows. EligibleJurisdictionModule reads investorCountry from the identity registry via the bound token. These external contracts from the @onchain-id/solidity package are not in scope, and incorrect behavior in identity verification or country resolution would directly affect token issuance, transfer restrictions, and wallet recovery.

Immediate Signer Rotation Without Timelock: The setAuthorisedSigner function in TortugaTRWA is gated by onlyOwner but executes immediately without any on-chain timelock. The authorisedSigner address controls all mint and burn operations via tortugaMint and tortugaBurn. A compromised owner key could rotate the signer to an attacker-controlled address, granting immediate and unrestricted minting authority. Off-chain controls provide deliberation, but no on-chain delay is enforced.

Unrestricted Module Policy Configuration: Module owners can modify enforcement parameters without timelock. The MaxHolderCountModule owner can call setMaxHolderCount to set the cap to 1, blocking all new holders. The MaxBalancePerHolderModule owner can call setMaxBalance with a cap of 1 wei, blocking all meaningful transfers. The FinmaInvestorCategoryModule owner can call setAllowedPair to disable all category pairs, freezing all transfers. The EligibleJurisdictionModule owner can remove all country codes, halting all transactions. These parameter changes take effect immediately with no delay for user reaction.

Forced Execution Without User Consent: The agent role in TortugaTRWA can execute forcedTransfer to move tokens between arbitrary verified addresses, recoveryAddress to reassign an investor's entire position to a new wallet, and setAddressFrozen / freezePartialTokens to restrict a holder's access to their tokens. All of these operations proceed without the affected holder's signature or approval, reflecting the concentrated enforcement authority inherent to a regulated security token.

Concentrated Administrative Authority in Owner Role: The owner role (Main Admin Safe) in TortugaTRWA controls signer rotation (setAuthorisedSigner), pause-safe rotation (setEmergencyPauseSafe), unpause authority (unpause), compliance and registry hot-swap lifecycle (queueComplianceSwap, executeComplianceSwap, queueIdentityRegistrySwap, executeIdentityRegistrySwap), SEV-1 declaration and clearing (declareSev1, clearSev1), agent management (addAgent, removeAgent), OnchainID updates (setOnchainID), token metadata (setName, setSymbol), and UUPS upgrades. Compromise of the owner key would grant complete protocol control including the ability to upgrade the implementation to arbitrary logic.

Module Ownership Consolidation: All six compliance modules inherit onlyOwner\-gated configuration from AbstractTortugaModule. The owner of each module controls policy parameters (holder caps, holding periods, investor categories, country allowlists, balance caps), migration gates (markMigrationComplete), UUPS upgrades, and ownership transfer. TortugaComplianceFactory transfers ownership of every deployed module and the compliance contract to a single _owner address. If the same Safe holds ownership across all modules and the token, a single compromise path affects the entire compliance stack.

Single Entity Upgrade Authority: Both TortugaTRWA and every concrete compliance module (MaxHolderCountModule, PropertyHoldingPeriodModule, FinmaInvestorCategoryModule, EligibleJurisdictionModule, MaxBalancePerHolderModule, SanctionsFreezeModule) implement UUPS upgradeability with _authorizeUpgrade gated solely by onlyOwner. The owner can deploy a new implementation with arbitrary logic and upgrade the proxy without any on-chain timelock, governance vote.

Flexibility and Risk in UUPS Proxy Architecture: All production contracts are deployed behind ERC-1967 UUPS proxies. TortugaTRWA uses UUPSUpgradeable with _authorizeUpgrade restricted to the owner. Every compliance module inherits _authorizeUpgrade from AbstractTortugaModule, also restricted to the owner. This architecture allows full logic replacement for the token and each compliance module. Upgrade execution is immediate once the owner calls upgradeTo or upgradeToAndCall, with no enforced on-chain delay or review period.

Upgrade Path Dependency via Storage Gaps and Reinitializer Versioning: TortugaTRWA uses reinitializer(2) for tortugaInit, creating a dependency on sequential initializer versioning across upgrades. The contract reserves a __gap[41] storage slot block, while AbstractTortugaModule reserves __gap[50] and each subclass reserves its own gap (ranging from __gap[46] to __gap[50]). Future upgrades must carefully coordinate gap reduction with new state variable placement across the two-level inheritance hierarchy (base module gap plus subclass gap). Incorrect gap management during an upgrade could corrupt storage layout across the compliance module stack.

Cross-Chain Authority Binding to ICP Canister: The authorisedSigner in TortugaTRWA is derived from an Internet Computer canister's Chain Fusion threshold ECDSA key. All mint and burn operations (tortugaMint, tortugaBurn) are gated by onlyAuthorisedSigner, meaning the canister must be operational and able to produce valid ECDSA signatures for any token issuance or redemption to proceed. Canister unavailability, key rotation delays, or threshold ECDSA liveness failures on the ICP side would halt all primary token operations on the satellite EVM chain.

SEV-1 Waiver Dependency on Off-Chain Incident Process: The declareSev1 and clearSev1 functions in TortugaTRWA allow the owner to bypass the 24-hour timelock on compliance and identity registry hot-swaps. The SEV-1 declaration consumes a single waiver on the next queueComplianceSwap or queueIdentityRegistrySwap call. No on-chain criteria define what constitutes a SEV-1 incident; the determination is entirely off-chain. Misuse or premature declaration could allow compliance or registry replacement without the deliberation window that the timelock is designed to provide.

Best-Effort Compliance Hooks on Emergency Paths: TortugaTRWA's forcedTransfer wraps the compliance transferred hook in a try/catch block, and holderSelfBurn wraps the destroyed hook similarly. If the compliance module's moduleTransferAction or moduleBurnAction reverts, the token-level operation still completes but the module's internal state (MaxHolderCountModule's currentHolderCount and trackedBalance, PropertyHoldingPeriodModule's firstAcquired and trackedBalance) becomes desynchronized from the token's actual balance state. While the modules use balanceOf as ground truth for enforcement decisions, the internal tracking mirrors used for off-chain reconciliation and migration operations would carry stale data forward into any subsequent migrateFrom call.

Migration Gate as Transfer Freeze Lever: The migrationComplete mapping in MaxHolderCountModule, PropertyHoldingPeriodModule, and FinmaInvestorCategoryModule gates all moduleCheck calls, returning false (transfer denied) until the module owner calls markMigrationComplete for the bound compliance. A delayed or omitted markMigrationComplete call after a compliance hot-swap would block all voluntary token transfers indefinitely. Conversely, premature markMigrationComplete before migrateFrom completes would allow transfers under empty (reset) policy state, potentially bypassing holding-period or holder-count constraints.

Premature Migration Completion Bypasses Holding-Period and FINMA Enforcement: PropertyHoldingPeriodModule's fail-closed gate is a single owner-toggled boolean with no on-chain coupling to the actual state migration performed by migrateFrom, and the module treats an empty firstAcquired clock as fully permissive (if (acquired 0) return true;). After a compliance hot-swap, every existing holder has an empty clock under the new compliance, and FinmaInvestorCategoryModule categories default to None (where allowedPair[None][None] is true). If markMigrationComplete is called before migrateFrom runs for the full holder set, moduleCheck returns true for everyone, silently disabling both the holding-period lockup and the FINMA category matrix. A holder still inside their regulatory lockup could then transfer freely, and because executed transfers cannot be unwound, the resulting regulatory violation is non-recoverable.

Findings

F-2026-1797Forced Transfer Bypasses Sanctions Freeze Check on the Recipient
Status
fixed
Severity

Low
F-2026-1791Module-Global Storage Breaks Per-Compliance Isolation
Status
accepted
Severity

Low
F-2026-1797Misleading Modifier Name: validMintPayload Applied to Burn Path
Status
fixed
Severity

Observation
F-2026-1797Unused Named Return Variable in forcedTransfer and recoveryAddress
Status
fixed
Severity

Observation
F-2026-1797Overridden setCompliance And setIdentityRegistry Rely On super For Authorization Instead Of A Local onlyOwner
Status
fixed
Severity

Observation
F-2026-1797Non-Atomic init Enables Owner Hijack Via Initializer Front-Running
Status
accepted
Severity

Observation
F-2026-1797Missing Allowance Check in transferFrom Function
Status
fixed
Severity

Observation
F-2026-1792Misleading Comment in moduleCheck Claims the Canister Gates On-Chain Transfers
Status
fixed
Severity

Observation
F-2026-1792 Mint and Burn Replay Payload Adds No Protection Beyond onlyAuthorisedSigner
Status
accepted
Severity

Observation
F-2026-1791Loop Counters Not Cached or Unchecked Increase Iteration Cost
Status
fixed
Severity

Observation
Code
―
Title
Status
Severity
F-2026-1797Forced Transfer Bypasses Sanctions Freeze Check on the Recipient
fixed

Low
F-2026-1791Module-Global Storage Breaks Per-Compliance Isolation
accepted

Low
F-2026-1797Misleading Modifier Name: validMintPayload Applied to Burn Path
fixed

Observation
F-2026-1797Unused Named Return Variable in forcedTransfer and recoveryAddress
fixed

Observation
F-2026-1797Overridden setCompliance And setIdentityRegistry Rely On super For Authorization Instead Of A Local onlyOwner
fixed

Observation
F-2026-1797Non-Atomic init Enables Owner Hijack Via Initializer Front-Running
accepted

Observation
F-2026-1797Missing Allowance Check in transferFrom Function
fixed

Observation
F-2026-1792Misleading Comment in moduleCheck Claims the Canister Gates On-Chain Transfers
fixed

Observation
F-2026-1792 Mint and Burn Replay Payload Adds No Protection Beyond onlyAuthorisedSigner
accepted

Observation
F-2026-1791Loop Counters Not Cached or Unchecked Increase Iteration Cost
fixed

Observation
1-10 of 12 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/tortuga-estate/trwa-contracts-hacken→
Commitb65764805a677b6bd6bc956ed9bf6c21844d2c1a
Final Commitf0b8e7b547f903ddadae8ac4da0bd749dc509e0a
WhitepaperN/A
Requirements./docs; NatSpec
Technical Requirements./docs; NatSpec

Assets in Scope

src
compliance
modules
AbstractTortugaModule.sol - src › compliance › modules › AbstractTortugaModule.sol
EligibleJurisdictionModule.sol - src › compliance › modules › EligibleJurisdictionModule.sol
FinmaInvestorCategoryModule.sol - src › compliance › modules › FinmaInvestorCategoryModule.sol
MaxBalancePerHolderModule.sol - src › compliance › modules › MaxBalancePerHolderModule.sol
MaxHolderCountModule.sol - src › compliance › modules › MaxHolderCountModule.sol
PropertyHoldingPeriodModule.sol - src › compliance › modules › PropertyHoldingPeriodModule.sol
SanctionsFreezeModule.sol - src › compliance › modules › SanctionsFreezeModule.sol
TortugaComplianceFactory.sol - src › compliance › TortugaComplianceFactory.sol
TortugaTRWA.sol - src › TortugaTRWA.sol
vendored
TokenVirtualPatched.sol - src › vendored › TokenVirtualPatched.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