All audits

Web3 · Smart contract audit

Trusset

softstack has completed 2 smart contract audits for Trusset between May 2026 and September 2026, reporting 55 findings in total (23 high, 22 medium and 10 low), of which 55 were resolved or mitigated before the final reports. Every report is published in the softstack public audit archive.

Key facts

Client
Trusset
Audits completed
2
First audited
Latest audit
Type
Smart contract audit
Chains
Ethereum
Languages
Solidity

About the project

Trusset is an open infrastructure protocol that turns static holdings such as stocks, commodities and real estate into programmable on-chain instruments. It is aimed at financial institutions working with tokenised real-world assets on Ethereum. The protocol is developed by Trusset UG, a company based in Berlin, Germany.

Findings

Combined across all 2 reviews softstack has completed for Trusset. Each report is broken out below.

0Critical
23High
22Medium
10Low
0Informational

55 of 55 findings resolved before the final reports.

Reports

Newest first. Every report covers its own scope and commit.

  1. Core Protocol smart contract audit

    · 41 files

    PDF
    0Critical
    13High
    17Medium
    8Low
    0Informational

    38 of 38 resolved

    ScopeSource repositorygithub.com/Trusset/trusset-erc3643-lending

    Components reviewed

    14 modules · 41 files
    1. CommodityCustody.sol
    2. BasicComplianceModule.sol
    3. IdentityRegistryUpgradeable.sol
    4. ICommodityToken.sol
    5. IComplianceModule.sol
    6. IIdentityRegistry.sol
    7. CustodyCollateralAdapter.sol
    8. Erc3643FreezeAdapter.sol
    9. AdapterDeployer.sol
    10. MarketTypes.sol
    11. SecurityAuctionModule.sol
    12. SecurityInsuranceFund.sol
    13. SecurityInterestRateModel.sol
    14. SecurityLendingVaultFactory.sol
    All 38 findings
    IDTitleSeverityStatus
    6.2.4removeLiquidity() Missing whenNotPaused, LP Bank Run During Emergency

    removeLiquidity() is callable during paused/emergency conditions, allowing LPs to withdraw in a way that can destabilize emergency handling.

    Highfixed
    6.2.6Raw ecrecover Without Signature Malleability Check, Double Mint Execution

    The contract performs signature verification using raw ecrecover and does not canonicalize or validate the s value and v parameters, which enables signature malleability and replay / double- execution of signed operations.

    Highfixed
    6.2.7_calculateCashRedemptionAmount Scale-Factor Risk / Precision Mismatch

    Fixed-point arithmetic mismatches and coefficient scaling in _calculateCashRedemptionAmount can cause rounding errors, off-by-magnitude miscalculations, and overflows when handling extreme values.

    Highfixed
    6.2.11Zombie shares let a wiped-out LP steal a fresh depositor's funds (ERC3643 Lending)

    totalShares is the denominator of LP share price. It is decremented only in removeLiquidity and is never reset when the pool is fully wiped out by bad debt (totalLiquidity == 0 && totalBorrowed == 0 && totalPendingLiquidationDebt == 0,…

    Highfixed
    6.2.13External T-REX compliance agent can bypass the market's collateral lock (ERC3643 Lending)

    Erc3643FreezeAdapter tracks locked collateral in its own refs[ref].amount counter, separate from the ERC-3643 token's own _frozenTokens counter. Any second agent registered on the token (ERC-3643 explicitly supports multiple agents, e.g.

    Highfixed
    6.2.19Accrued LP interest is double-counted after liquidation (ERC3643 Lending)

    globalAccruedLPInterest accumulates the LP share of interest as it accrues. On the repay path, _distributeInterest both credits totalLiquidity and decrements globalAccruedLPInterest by the realized amount, keeping the two in sync.

    Mediumfixed
    6.2.20Router identity-registry lapse permanently blocks instant liquidation (ERC3643 Lending)

    When a market uses instant liquidation (useDutchAuction == false, the default), _instantLiquidation calls adapter.enforce(ref, liquidationContract, amount), which calls token.forcedTransfer(holder, liquidationContract, amount).

    Mediumfixed
    6.2.21Borrower can repurchase their own collateral at the Dutch auction floor price (ERC3643 Lending)

    bidOnAuction has no guard preventing msg.sender from being the loan's own borrower. In Erc3643FreezeAdapter.enforce, the branch if (to != holder) skips the forcedTransfer call entirely when the bidder is the same address as the…

    Mediumfixed
    6.2.22All eight upgradeable contracts are missing storage gap arrays (ERC3643 Lending)

    All 8 upgradeable contracts declare state variables sequentially with no uint256[N] private __gap reserve.

    Mediumfixed
    6.2.23Oracle deviation circuit breaker only bounds a single hop, not cumulative movement (ERC3643 Lending)

    _checkDeviation computes the allowed price movement against the current stored price only (maxAllowed = currentPrice * maxDeviationBps / BPS_PRECISION).

    Mediumfixed
    6.2.25Vault performance fee crystallizes on unrealized, operator-controllable marks (ERC3643 Lending)

    collectPerformanceFee prices vault shares off _totalAssets(), which sums idle assets and each allocated market's getLpInfo().value.

    Mediumfixed
    6.2.26Single-step ownership and unguarded renounceOwnership can permanently brick the insurance fund (ERC3643 Lending)

    SecurityInsuranceFund inherits standard OwnableUpgradeable, whose unguarded renounceOwnership() sets owner = address(0) with no way back.

    Mediumfixed
    6.2.27Interest rate changes retroactively re-price the already-open accrual window (ERC3643 Lending)

    SecurityInterestRateModel.updateParameters (owned by the issuer) changes the rate curve immediately, with no timelock and no pre-change accrual checkpoint.

    Mediumfixed
    6.2.28Expired Dutch auction stays biddable at the price frozen at auction creation (ERC3643 Lending)

    bidOnAuction gates on three conditions: the auction being active, a non-zero amount, and a clamp to collateralRemaining.

    Mediumfixed
    6.2.29Liquidation surplus is measured against the oracle mark, so a defaulted borrower is paid cash the insurance fund covered (ERC3643 Lending)

    On the instant liquidation path, when the seizure consumes the entire collateral position, _instantLiquidation derives debtRepaid from the oracle mark of the seized collateral alone: debtRepaid = collateralValueSeized / (1 + penalty) at…

    Mediumfixed
    6.2.30Dutch auction caps the pool's take per bid instead of per auction (ERC3643 Lending)

    bidOnAuction decides how much of a bid the pool may keep on a per bid basis rather than per auction.

    Mediumfixed
    6.2.31Raw IERC20.transfer() for USDC Payout in CommodityTokenUpgradeable

    The contract performs value transfers using raw IERC20.transfer / transferFrom calls and assumes a bool return or silent success.

    Lowfixed
    6.2.32CEI Violation in CommodityTokenSale, State Updates After External ETH Transfers

    The contract performs an external ETH transfer (or token transfer) to a recipient before updating internal accounting that records purchase/sale state.

    Lowfixed
    6.2.33Integer-division truncation locks the price oracle at 1 wei (ERC3643 Lending)

    The contract performs value transfers using raw IERC20.transfer / transferFrom calls and assumes a bool return or silent success.

    Lowfixed
    6.2.34Factory default risk configuration is validated, evented and readable but never reaches a deployed market (ERC3643 Lending)

    defaultRiskConfig is write-only state. It is declared at SecurityTokenLendingFactory.sol:144, initialized in the constructor at :251-259, written by the validated onlyOwner setter at :707-727 which emits DefaultRiskConfigUpdated, and read…

    Lowfixed
    6.2.35Curator nomination is not scoped to the market's own deployer (ERC3643 Lending)

    nominateCurator and withdrawNomination authorize on isAuthorizedDeployer(msg.sender), which is a factory-wide flag set by the owner through setDeployer.

    Lowfixed
    6.2.36Ceiling-rounded burn shares void the vault's utilization-capped market draw (ERC3643 Lending)

    _drawFromMarket sizes the draw at the market's exact free liquidity (if (draw > available) draw = available) and then converts that amount to LP shares with a ceiling, _mulDivUp(draw, lpShares, lpValue).

    Lowfixed
    6.2.37Zero-proceeds settlement closes the router record before the sale is realized (ERC3643 Lending)

    settleLiquidation writes pending.settled = true at line 329 before it validates anything about the liquidation's realization state.

    Lowfixed
    6.2.38Revoking the freeze adapter's agent role strands both liquidation and settlement (ERC3643 Lending)

    Erc3643FreezeAdapter depends on holding the agent role on the ERC-3643 token to call freezePartialTokens, unfreezePartialTokens, and forcedTransfer. The adapter has no isAgent probe, no circuit breaker, and no fallback.

    Lowfixed
    6.2.1Raw IERC20.transfer/transferFrom Without SafeERC20

    The contract performs value transfers using raw IERC20.transfer / transferFrom calls and assumes a bool return or silent success.

    Highfixed
    6.2.2Empty Catch Swallows Lending Market Callback, Silent Fund Misrouting

    An empty try { ... } catch {} or a silent catch block is swallowing failures originating from external callbacks (e.g., lending market callbacks).

    Highfixed
    6.2.3Oracle Admin Can Crash Price via setMaxDeviation/updatePrice

    Oracle functions setMaxDeviation and updatePrice can be used in quick succession to bypass intended circuit breakers and set price to artificially low/high levels.

    Highfixed
    6.2.5Dutch Auction Stale Collateral Price Creates Systematic Bad Debt

    The auction workflow relies on a collateral price snapshot that can remain unchanged throughout the auction lifecycle instead of forcing a fresh pricing check when liquidation is finalized.

    Highfixed
    6.2.8ISSUER_ROLE Can KYC-Bypass via addAuthorizedContract

    The ISSUER_ROLE can call addAuthorizedContract, potentially authorizing contracts that bypass KYC checks. Root cause: Role-scoped permissions that grant overly broad authority to add trusted contracts without cross-checks.

    Highfixed
    6.2.9CommodityCustody USDC Permanently Locked in Router

    Funds sent to the router can be trapped when downstream callbacks fail and the router lacks compensating refund/path recovery.

    Highfixed
    6.2.10Stock Split Sub-Issuer Accounting Desynchronization

    stockSplit() doubles holder balances but never adjusts _subIssuerMinted or _subIssuerCap, so the token's sub-issuer accounting becomes permanently stale immediately after any forward split.

    Highfixed
    6.2.12Insurance fund is drained without reimbursement on late liquidation settlement (ERC3643 Lending)

    When a router-based liquidation is not settled within MAX_LIQUIDATION_AGE (7 days), the admin can call handleLiquidationTimeout, which sets pending.settled = true, draws USDC from the insurance fund via coverBadDebt, and credits the pool…

    Highfixed
    6.2.14Dutch Auction Stale Price Window, Liquidation at Unfair Price

    The auction uses a stored endPrice computed earlier and does not re-check oracle price at settlement time, enabling bidders to win auctions priced on stale data.

    Mediumfixed
    6.2.15Settlement Operator Single-Key Unlimited Power

    The settlement operator currently has broad, unilateral permissions that allow it to finalize settlements, route funds, and change bookkeeping without multi-party checks.

    Mediumfixed
    6.2.16Oracle Timestamp Regression Allows Stale Price Replay

    Oracle updates accept an updatedAt field from off-chain sources; if the provider allows non- monotonic timestamps, an attacker can replay stale signed price updates or submit earlier timestamps that pass naive checks.

    Mediumfixed
    6.2.17Stock Split Duplicate Holder Processing

    Processing holder arrays with duplicate addresses causes double-counting and incorrect balance allocations during split operations.

    Mediumfixed
    6.2.18emergencyWithdraw() No Timelock, Instant Fund Drainage

    An emergencyWithdraw() function permits immediate withdrawals by an admin without timelock, allowing instant large fund movements. Root cause: Privileged function lacking governance/time-delays and threshold checks.

    Mediumfixed
    6.2.24Open-market curator adoption is front-runnable (ERC3643 Lending)

    SecurityTokenLendingFactory.acceptCuratorRole is the single, irreversible handover of every privileged role on a freshly deployed lending set.

    Mediumfixed
  2. Core Protocol smart contract audit

    · 14 files

    PDF
    0Critical
    10High
    5Medium
    2Low
    0Informational

    17 of 17 resolved

    ScopeSource repositorygithub.com/trusset/trusset-stock-lending

    Components reviewed

    14 modules · 14 files
    1. CommodityCustody.sol
    2. BasicComplianceModule.sol
    3. IdentityRegistryUpgradeable.sol
    4. ICommodityToken.sol
    5. IComplianceModule.sol
    6. IIdentityRegistry.sol
    7. ISecurityToken.sol
    8. StockToken.sol
    9. StockPriceOracle.sol
    10. StockLendingMarket.sol
    11. CommodityTokenUpgradeable.sol
    12. StockInsuranceFund.sol
    13. StockLendingFactory.sol
    14. CommodityTokenSale.sol
    All 17 findings
    IDTitleSeverityStatus
    6.2.4removeLiquidity() Missing whenNotPaused, LP Bank Run During Emergency

    removeLiquidity() is callable during paused/emergency conditions, allowing LPs to withdraw in a way that can destabilize emergency handling.

    Highfixed
    6.2.6Raw ecrecover Without Signature Malleability Check, Double Mint Execution

    The contract performs signature verification using raw ecrecover and does not canonicalize or validate the s value and v parameters, which enables signature malleability and replay / double- execution of signed operations.

    Highfixed
    6.2.7_calculateCashRedemptionAmount Scale-Factor Risk / Precision Mismatch

    Fixed-point arithmetic mismatches and coefficient scaling in _calculateCashRedemptionAmount can cause rounding errors, off-by-magnitude miscalculations, and overflows when handling extreme values.

    Highfixed
    6.2.16Raw IERC20.transfer() for USDC Payout in CommodityTokenUpgradeable

    The contract performs value transfers using raw IERC20.transfer / transferFrom calls and assumes a bool return or silent success.

    Lowfixed
    6.2.17CEI Violation in CommodityTokenSale, State Updates After External ETH Transfers

    The contract performs an external ETH transfer (or token transfer) to a recipient before updating internal accounting that records purchase/sale state.

    Lowfixed
    6.2.1Raw IERC20.transfer/transferFrom Without SafeERC20

    The contract performs value transfers using raw IERC20.transfer / transferFrom calls and assumes a bool return or silent success.

    Highfixed
    6.2.2Empty Catch Swallows Lending Market Callback, Silent Fund Misrouting

    An empty try { ... } catch {} or a silent catch block is swallowing failures originating from external callbacks (e.g., lending market callbacks).

    Highfixed
    6.2.3Oracle Admin Can Crash Price via setMaxDeviation/updatePrice

    Oracle functions setMaxDeviation and updatePrice can be used in quick succession to bypass intended circuit breakers and set price to artificially low/high levels.

    Highfixed
    6.2.5Dutch Auction Stale Collateral Price Creates Systematic Bad Debt

    The auction workflow relies on a collateral price snapshot that can remain unchanged throughout the auction lifecycle instead of forcing a fresh pricing check when liquidation is finalized.

    Highfixed
    6.2.8ISSUER_ROLE Can KYC-Bypass via addAuthorizedContract

    The ISSUER_ROLE can call addAuthorizedContract, potentially authorizing contracts that bypass KYC checks. Root cause: Role-scoped permissions that grant overly broad authority to add trusted contracts without cross-checks.

    Highfixed
    6.2.9CommodityCustody USDC Permanently Locked in Router

    Funds sent to the router can be trapped when downstream callbacks fail and the router lacks compensating refund/path recovery.

    Highfixed
    6.2.10Stock Split Sub-Issuer Accounting Desynchronization

    stockSplit() doubles holder balances but never adjusts _subIssuerMinted or _subIssuerCap, so the token's sub-issuer accounting becomes permanently stale immediately after any forward split.

    Highfixed
    6.2.11Dutch Auction Stale Price Window, Liquidation at Unfair Price

    The auction uses a stored endPrice computed earlier and does not re-check oracle price at settlement time, enabling bidders to win auctions priced on stale data.

    Mediumfixed
    6.2.12Settlement Operator Single-Key Unlimited Power

    The settlement operator currently has broad, unilateral permissions that allow it to finalize settlements, route funds, and change bookkeeping without multi-party checks.

    Mediumfixed
    6.2.13Oracle Timestamp Regression Allows Stale Price Replay

    Oracle updates accept an updatedAt field from off-chain sources; if the provider allows non- monotonic timestamps, an attacker can replay stale signed price updates or submit earlier timestamps that pass naive checks.

    Mediumfixed
    6.2.14Stock Split Duplicate Holder Processing

    Processing holder arrays with duplicate addresses causes double-counting and incorrect balance allocations during split operations.

    Mediumfixed
    6.2.15emergencyWithdraw() No Timelock, Instant Fund Drainage

    An emergencyWithdraw() function permits immediate withdrawals by an admin without timelock, allowing instant large fund movements. Root cause: Privileged function lacking governance/time-delays and threshold checks.

    Mediumfixed

Methodology

Review process

  1. Scoping and threat modelling

    The commit hash is frozen and the attack surface is mapped: asset flows, trust assumptions, privileged roles and the external systems the code depends on.

  2. Automated analysis

    Static analysers sweep the full codebase to surface known weakness patterns, unreachable states and deviations from established conventions.

  3. Manual code review

    Independent engineers read the code line by line, concentrating on business logic, access control, arithmetic and the handling of external calls.

  4. Adversarial testing

    Property-based fuzzing and invariant tests attack the assumptions the protocol relies on, targeting states that ordinary unit tests never reach.

  5. Reporting

    Every finding is documented with a severity rating, its impact, the steps to reproduce it and a concrete remediation the team can act on.

  6. Remediation verification

    Submitted fixes are re-reviewed against the original finding, and the final report records the resolved state of each one.

FAQ

2 separate reviews, the first in May 2026 and the most recent in September 2026. Each one covers its own scope and commit, and is listed individually on this page.

55 findings across all 2 reviews: 23 high, 22 medium and 10 low. 55 of 55 were resolved or mitigated before the final reports.

Each report covers the code as it stood at the commit it names, the most recent in September 2026. Any code changed after that point is outside the reviewed scope.