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.
55 of 55 findings resolved before the final reports.
Reports
Newest first. Every report covers its own scope and commit.
Core Protocol smart contract audit
0Critical13High17Medium8Low0Informational38 of 38 resolved
Scope
Source repositorygithub.com/Trusset/trusset-erc3643-lendingComponents reviewed
14 modules · 41 files- CommodityCustody.sol
- BasicComplianceModule.sol
- IdentityRegistryUpgradeable.sol
- ICommodityToken.sol
- IComplianceModule.sol
- IIdentityRegistry.sol
- CustodyCollateralAdapter.sol
- Erc3643FreezeAdapter.sol
- AdapterDeployer.sol
- MarketTypes.sol
- SecurityAuctionModule.sol
- SecurityInsuranceFund.sol
- SecurityInterestRateModel.sol
- SecurityLendingVaultFactory.sol
All 38 findings
ID Title Severity Status 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.
High fixed 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.
High fixed 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.
High fixed 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,…
High fixed 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.
High fixed 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.
Medium fixed 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).
Medium fixed 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…
Medium fixed 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.
Medium fixed 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).
Medium fixed 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.
Medium fixed 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.
Medium fixed 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.
Medium fixed 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.
Medium fixed 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…
Medium fixed 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.
Medium fixed 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.
Low fixed 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.
Low fixed 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.
Low fixed 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…
Low fixed 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.
Low fixed 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).
Low fixed 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.
Low fixed 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.
Low fixed 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.
High fixed 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).
High fixed 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.
High fixed 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.
High fixed 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.
High fixed 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.
High fixed 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.
High fixed 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…
High fixed 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.
Medium fixed 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.
Medium fixed 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.
Medium fixed 6.2.17Stock Split Duplicate Holder Processing Processing holder arrays with duplicate addresses causes double-counting and incorrect balance allocations during split operations.
Medium fixed 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.
Medium fixed 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.
Medium fixed Core Protocol smart contract audit
0Critical10High5Medium2Low0Informational17 of 17 resolved
Scope
Source repositorygithub.com/trusset/trusset-stock-lendingComponents reviewed
14 modules · 14 files- CommodityCustody.sol
- BasicComplianceModule.sol
- IdentityRegistryUpgradeable.sol
- ICommodityToken.sol
- IComplianceModule.sol
- IIdentityRegistry.sol
- ISecurityToken.sol
- StockToken.sol
- StockPriceOracle.sol
- StockLendingMarket.sol
- CommodityTokenUpgradeable.sol
- StockInsuranceFund.sol
- StockLendingFactory.sol
- CommodityTokenSale.sol
All 17 findings
ID Title Severity Status 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.
High fixed 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.
High fixed 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.
High fixed 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.
Low fixed 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.
Low fixed 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.
High fixed 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).
High fixed 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.
High fixed 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.
High fixed 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.
High fixed 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.
High fixed 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.
High fixed 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.
Medium fixed 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.
Medium fixed 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.
Medium fixed 6.2.14Stock Split Duplicate Holder Processing Processing holder arrays with duplicate addresses causes double-counting and incorrect balance allocations during split operations.
Medium fixed 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.
Medium fixed
Methodology
Review process
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.
Automated analysis
Static analysers sweep the full codebase to surface known weakness patterns, unreachable states and deviations from established conventions.
Manual code review
Independent engineers read the code line by line, concentrating on business logic, access control, arithmetic and the handling of external calls.
Adversarial testing
Property-based fuzzing and invariant tests attack the assumptions the protocol relies on, targeting states that ordinary unit tests never reach.
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.
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.