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 | |
|---|---|
| Name | Smart Contract Code Review and Security Analysis Report for Tortuga Estate |
| Audited By | Khrystyna Tkachuk, Kornel Światłowski |
| Approved By | Seher Saylik |
| Website | https://tortuga.estate→ |
| Changelog | 06/07/2026 - Preliminary Report |
| 31/07/2026 - Final Report | |
| Platform | Ethereum; Base; Plume |
| Language | Solidity |
| Tags | RWA; Upgradable; ERC3643 |
| Methodology | https://docs.hacken.io/methodologies/smart-contracts→ |
Document
- Name
- Smart Contract Code Review and Security Analysis Report for Tortuga Estate
- Audited By
- Khrystyna Tkachuk, Kornel Światłowski
- Approved By
- Seher Saylik
- Website
- https://tortuga.estate→
- Changelog
- 06/07/2026 - Preliminary Report
- 31/07/2026 - Final Report
- Platform
- Ethereum; Base; Plume
- Language
- Solidity
- Tags
- RWA; Upgradable; ERC3643
Review Scope | |
|---|---|
| Repository | https://github.com/tortuga-estate/trwa-contracts-hacken→ |
| Commit | b657648 |
| Final Commit | f0b8e7b |
Review Scope
- Commit
- b657648
- Final Commit
- f0b8e7b
Audit Summary
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.solvendors 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
setAuthorisedSignerto rotate the canister-derived Chain Fusion ECDSA signer address (immediate, no timelock).Can call
setEmergencyPauseSafeto rotate the emergency pause Safe address.Can call
unpauseto resume token operations after a pause.Can call
declareSev1to waive the 24-hour timelock on the next compliance or registry swap.Can call
clearSev1to revoke a previously declared SEV-1 waiver and retroactively reinstate the timelock on any in-flight pending swap.Can call
queueComplianceSwapto queue a new compliance contract for hot-swap with a 24-hour timelock (or immediate under SEV-1).Can call
cancelComplianceSwapto cancel an in-flight compliance hot-swap.Can call
executeComplianceSwapto finalize a timelocked compliance hot-swap.Can call
queueIdentityRegistrySwapto queue a new identity registry for hot-swap with a 24-hour timelock (or immediate under SEV-1).Can call
cancelIdentityRegistrySwapto cancel an in-flight identity registry swap.Can call
executeIdentityRegistrySwapto finalize a timelocked identity registry hot-swap.Can call
setOnchainIDto update the token's OnchainID address.Can call
setNameto update the token name (inherited from TokenVirtualPatched).Can call
setSymbolto update the token symbol (inherited from TokenVirtualPatched).Can call
transferOwnershipto transfer contract ownership to a new address (inherited from OwnableUpgradeable).Can call
addAgentandremoveAgentto manage the agent role set (inherited from AgentRoleUpgradeable).UUPS upgrade authority:
_authorizeUpgraderestricts implementation upgrades to the owner.renounceOwnershipis explicitly blocked.
authorisedSigner: The canister-derived Chain Fusion ECDSA address. The only address permitted to mint and burn tokens.
Can call
tortugaMintto mint tokens to a verified address with replay-protected payload (nonce, chainId, propertyId, expiry).Can call
tortugaBurnto 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
pauseto halt all non-emergency token operations.
agent (inherited from TokenVirtualPatched via AgentRoleUpgradeable): Addresses granted the agent role by the owner.
Can call
forcedTransferto execute emergency token transfers (court-ordered seizure, sanctions liquidation) with best-effort compliance hooks.Can call
recoveryAddressto 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
setAddressFrozento freeze or unfreeze an address entirely.Can call
freezePartialTokensto freeze a specified amount of tokens for an address.Can call
unfreezePartialTokensto unfreeze a specified amount of tokens for an address.Can call
batchSetAddressFrozen,batchFreezePartialTokens,batchUnfreezePartialTokens, andbatchForcedTransferfor batch operations (inherited from TokenVirtualPatched).
MaxHolderCountModule.sol:
owner (inherited from AbstractTortugaModule via AbstractModuleUpgradeable / OwnableUpgradeable): Module administrator.
Can call
setMaxHolderCountto update the maximum number of unique holders allowed.Can call
markMigrationCompleteto unlock rule enforcement for a specific compliance binding.Can call
transferOwnershipto transfer module ownership.UUPS upgrade authority:
_authorizeUpgraderestricts implementation upgrades to the owner (inherited from AbstractTortugaModule).renounceOwnershipis explicitly blocked (inherited from AbstractTortugaModule).
compliance (via
onlyComplianceCallmodifier inherited from AbstractModuleUpgradeable): The bound compliance contract.Can call
moduleMintAction,moduleTransferAction,moduleBurnActionto update holder-count tracking after token state changes.Can call
migrateFromto copy holder-set state from a prior module instance during compliance hot-swap.Can call
migrateForRecoveredWallet(deliberate no-op that emitsRecoveryMigrationNoop; holder-count accuracy is maintained bymoduleTransferActionvia 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
migrateForRecoveredWalletFromTokento trigger recovery-time tracked-balance migration from within the token'srecoveryAddressflow.
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
_ownerparameter duringdeployForProperty. Any address may calldeployForProperty.
PropertyHoldingPeriodModule.sol:
owner (inherited from AbstractTortugaModule via AbstractModuleUpgradeable / OwnableUpgradeable): Module administrator.
Can call
setMinHoldingPeriodto update the minimum holding period (capped atMAX_HOLDING_PERIODof 3,650 days).Can call
markMigrationCompleteto unlock rule enforcement for a specific compliance binding.Can call
transferOwnershipto transfer module ownership.UUPS upgrade authority:
_authorizeUpgraderestricts implementation upgrades to the owner (inherited from AbstractTortugaModule).renounceOwnershipis explicitly blocked (inherited from AbstractTortugaModule).
compliance (via
onlyComplianceCall): The bound compliance contract.Can call
moduleMintAction,moduleTransferAction,moduleBurnActionto update per-holder acquisition timestamps and tracked balances.Can call
migrateFromto copy holding-period state from a prior module instance during compliance hot-swap.Can call
migrateForRecoveredWalletto 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
migrateForRecoveredWalletFromTokento trigger recovery-time holding-period migration from within the token'srecoveryAddressflow.
FinmaInvestorCategoryModule.sol:
owner (inherited from AbstractTortugaModule via AbstractModuleUpgradeable / OwnableUpgradeable): Module administrator.
Can call
setHolderCategoryto assign a FINMA investor category (Retail, Qualified, Institutional) to a holder under a specific compliance.Can call
batchSetHolderCategoryto assign categories to multiple holders in a single transaction.Can call
setAllowedPairto configure the sender-to-recipient category policy matrix.Can call
markMigrationCompleteto unlock rule enforcement for a specific compliance binding.Can call
migrateFromPriorModuleto copy holder-category assignments from a previous module instance across compliance pairs.Can call
migrateForRecoveredWalletto copy the lost wallet's FINMA category to the new wallet during recovery.Can call
transferOwnershipto transfer module ownership.UUPS upgrade authority:
_authorizeUpgraderestricts implementation upgrades to the owner (inherited from AbstractTortugaModule).renounceOwnershipis 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
migrateForRecoveredWalletFromTokento trigger recovery-time FINMA category migration from within the token'srecoveryAddressflow.
EligibleJurisdictionModule.sol:
owner (inherited from AbstractTortugaModule via AbstractModuleUpgradeable / OwnableUpgradeable): Module administrator.
Can call
setCountryEligibilityto enable or disable a country code in the jurisdiction allowlist (country code 0 can never be enabled).Can call
batchSetCountryEligibilityto update eligibility for multiple country codes in a single transaction.Can call
transferOwnershipto transfer module ownership.UUPS upgrade authority:
_authorizeUpgraderestricts implementation upgrades to the owner (inherited from AbstractTortugaModule).renounceOwnershipis 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
setMaxBalanceto set the per-holder token balance cap for a specific compliance binding.Can call
transferOwnershipto transfer module ownership.UUPS upgrade authority:
_authorizeUpgraderestricts implementation upgrades to the owner (inherited from AbstractTortugaModule).renounceOwnershipis 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
transferOwnershipto transfer module ownership.UUPS upgrade authority:
_authorizeUpgraderestricts implementation upgrades to the owner (inherited from AbstractTortugaModule).renounceOwnershipis 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
Code ― | Title | Status | Severity | |
|---|---|---|---|---|
| F-2026-1797 | Forced Transfer Bypasses Sanctions Freeze Check on the Recipient | fixed | Low | |
| F-2026-1791 | Module-Global Storage Breaks Per-Compliance Isolation | accepted | Low | |
| F-2026-1797 | Misleading Modifier Name: validMintPayload Applied to Burn Path | fixed | Observation | |
| F-2026-1797 | Unused Named Return Variable in forcedTransfer and recoveryAddress | fixed | Observation | |
| F-2026-1797 | Overridden setCompliance And setIdentityRegistry Rely On super For Authorization Instead Of A Local onlyOwner | fixed | Observation | |
| F-2026-1797 | Non-Atomic init Enables Owner Hijack Via Initializer Front-Running | accepted | Observation | |
| F-2026-1797 | Missing Allowance Check in transferFrom Function | fixed | Observation | |
| F-2026-1792 | Misleading 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-1791 | Loop Counters Not Cached or Unchecked Increase Iteration Cost | fixed | Observation |
Appendix 1. Definitions
Severities
When auditing smart contracts, Hacken is using a risk-based approach that considers Likelihood, Impact, Exploitability and Complexity metrics to evaluate findings and score severities.
Reference on how risk scoring is done is available through the repository in our Github organization:
Severity | Description |
|---|---|
Critical | Critical vulnerabilities are usually straightforward to exploit and can lead to the loss of user funds or contract state manipulation. |
High | High vulnerabilities are usually harder to exploit, requiring specific conditions, or have a more limited scope, but can still lead to the loss of user funds or contract state manipulation. |
Medium | Medium vulnerabilities are usually limited to state manipulations and, in most cases, cannot lead to asset loss. Contradictions and requirements violations. Major deviations from best practices are also in this category. |
Low | Major deviations from best practices or major Gas inefficiency. These issues will not have a significant impact on code execution. |
Severity
- Critical
Description
- Critical vulnerabilities are usually straightforward to exploit and can lead to the loss of user funds or contract state manipulation.
Severity
- High
Description
- High vulnerabilities are usually harder to exploit, requiring specific conditions, or have a more limited scope, but can still lead to the loss of user funds or contract state manipulation.
Severity
- Medium
Description
- Medium vulnerabilities are usually limited to state manipulations and, in most cases, cannot lead to asset loss. Contradictions and requirements violations. Major deviations from best practices are also in this category.
Severity
- Low
Description
- Major deviations from best practices or major Gas inefficiency. These issues will not have a significant impact on code execution.
Potential Risks
The "Potential Risks" section identifies issues that are not direct security vulnerabilities but could still affect the project’s performance, reliability, or user trust. These risks arise from design choices, architectural decisions, or operational practices that, while not immediately exploitable, may lead to problems under certain conditions. Additionally, potential risks can impact the quality of the audit itself, as they may involve external factors or components beyond the scope of the audit, leading to incomplete assessments or oversight of key areas. This section aims to provide a broader perspective on factors that could affect the project's long-term security, functionality, and the comprehensiveness of the audit findings.
Appendix 2. Scope
The scope of the project includes the following smart contracts from the provided repository:
Scope Details | |
|---|---|
| Repository | https://github.com/tortuga-estate/trwa-contracts-hacken→ |
| Commit | b65764805a677b6bd6bc956ed9bf6c21844d2c1a |
| Final Commit | f0b8e7b547f903ddadae8ac4da0bd749dc509e0a |
| Whitepaper | N/A |
| Requirements | ./docs; NatSpec |
| Technical Requirements | ./docs; NatSpec |
Scope Details
- Commit
- b65764805a677b6bd6bc956ed9bf6c21844d2c1a
- Final Commit
- f0b8e7b547f903ddadae8ac4da0bd749dc509e0a
- Whitepaper
- N/A
- Requirements
- ./docs; NatSpec
- Technical Requirements
- ./docs; NatSpec
Assets in Scope
Appendix 3. Additional Valuables
Additional Recommendations
The smart contracts in the scope of this audit could benefit from the introduction of automatic emergency actions for critical activities, such as unauthorized operations like ownership changes or proxy upgrades, as well as unexpected fund manipulations, including large withdrawals or minting events. Adding such mechanisms would enable the protocol to react automatically to unusual activity, ensuring that the contract remains secure and functions as intended.
To improve functionality, these emergency actions could be designed to trigger under specific conditions, such as:
Detecting changes to ownership or critical permissions.
Monitoring large or unexpected transactions and minting events.
Pausing operations when irregularities are identified.
These enhancements would provide an added layer of security, making the contract more robust and better equipped to handle unexpected situations while maintaining smooth operations.
Frameworks and Methodologies
This security assessment was conducted in alignment with recognised penetration testing standards, methodologies and guidelines, including the NIST SP 800-115 – Technical Guide to Information Security Testing and Assessment →, and the Penetration Testing Execution Standard (PTES) →, These assets provide a structured foundation for planning, executing, and documenting technical evaluations such as vulnerability assessments, exploitation activities, and security code reviews. Hacken’s internal penetration testing methodology extends these principles to Web2 and Web3 environments to ensure consistency, repeatability, and verifiable outcomes.