As of 30 Oct 2026, Strata has 2 audit reports by 1 security firm on Trustblock; the latest is by Quantstamp, dated 11 Aug 2026. Those reports list no open critical findings.
Record last updated . It holds audits and findings, contracts and incidents up to Oct 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Strata Markets Update - Isolated and Saturn Strate | Security firm: Quantstamp | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · certificate.quantstamp.com |
| Strata - Discrete Accounting | Security firm: Quantstamp | Date: | Scope: Not recorded (see the report) | No open critical findings | Open · certificate.quantstamp.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 0 |
| Medium | 1 | 0 | 7 |
| Low | 0 | 5 | 14 |
The function `SaturnCooldownRequestImpl.finalize()` calls `sUSDat.claim()`, which claims every processed withdrawal NFT owned by the proxy rather than the specific `requestId` created for the current withdrawal. An attacker can transfer a smaller processed withdrawal NFT to the proxy, causing `finalize()` to succeed while the proxy's actual request is still pending. `UnstakeCooldown` then removes the active request and returns the proxy to its pool, leaving the real withdrawal NFT without a tracked finalization path. Once that request is eventually processed, the user's funds remain in the proxy until the proxy contract is reused within `UnstakeCooldown` and gets finalized as part of a later withdrawal.
Open the reportThe `IsolatedStrategy.imbalances()` function is intended to set the Junior strategy target to the greater of Junior's tranche-level accounting entitlement and the configured minimum liquid allocation. Instead, whenever the Junior strategy's physical assets are below the floor amount, the function unconditionally replaces `jrtNavT0` with that floor amount. If Junior's entitlement is already greater than the floor, this lowers the target rather than preserving the larger entitlement, this is contrary to the behaviour outlined in the SIP-06 specification. For example, consider Junior and Senior entitlements of 1,000 and 100, while their strategies physically hold 100 and 1,000. With a 30% floor, the floor amount is 330. The Junior strategy's full deficit relative to its entitlement is 900, but the function initially reports only 230. A call to `initiateRebalanceByDebt()` therefore moves only enough assets to bring the Junior strategy to the floor. Once that pending or completed rebalance is included in Junior's effective strategy balance, the floor branch stops applying and the remaining 670 deficit becomes visible, requiring a second rebalance. Consequently, a nominal full-debt rebalance may require two operations and potentially two asynchronous withdrawal requests. This increases operational complexity and can leave more of Junior's economic backing exposed to the Senior strategy for longer than intended, particularly when the Senior strategy has a lengthy withdrawal cooldown.
Open the report`SharesCooldown` escrows redemption requests during their cooldown and lets each request attach optional `strategyOptions` (custom redemption parameters), stored in `requestMeta[vault][to][metaKey]` and referenced by a per-request `metaKey`, where the sentinel `metaKey == bytes12(0)` means "no options" and a real options request gets a unique key from a per-`(vault, to)` nonce. `metaKey == bytes12(0)` is the reserved "no `strategyOptions`" sentinel. But the per-`(vault, to)` metadata nonce post-increments from zero: ```solidity uint96 nonce = _metaKeyNonce[address(vault)][to]++; metaKey = bytes12(uint96(nonce)); requestMeta[address(vault)][to][metaKey] = TRequestMeta({ strategyOptions: strategyOptions }); ``` So the first options-bearing request for a `(vault, to)` derives `metaKey = bytes12(uint96(0)) = bytes12(0)`, indistinguishable from a no-metadata request, and stores its options at the sentinel slot `requestMeta[vault][to][bytes12(0)]`. The finalize/cancel functions only delete the metadata when `metaKey != bytes12(0)`, so that first request's metadata with `metaKey = bytes12(0)` is never deleted. Because every plain (no-options) request also carries `metaKey == bytes12(0)`, all of them read that one shared slot at finalize time and pick up the options that do not belong to their request i.e. wrong route/slippage/sub-strategy, producing unfavorable terms or a revert.
Open the reportBetween oracle updates, `DYSAccounting` grows Senior's NAV toward its target APR (`aprSrt`) by shifting value from Junior to Senior on paper, accumulating the unearned gain in `srtPnLProjected`. At the next reconciliation it reverses that `srtPnLProjected` against the pooled Senior base and returns it to Junior (`projUndo = Math.min(srtPnLProjected, srtNavT0); srtNavT0 -= projUndo; jrtNavT0Real += projUndo`), only consistent when every Senior that received the projected gain is still present. The flaw: a Senior can leave first. Its `maxWithdraw()` includes the projected gain, but `updateBalanceFlow()` does not decrement `srtPnLProjected`, so the assets leave while the `srtPnLProjected` stays the same and the pool-wide reversal charges the remaining Senior for a gain a departed Senior already carried out. Total NAV still balances (`navT1 == jrt + srt + reserve`), so value is silently redistributed rather than reverting. Who absorbs it depends on how much Senior stayed: 1. Partial exit (cap does not bind, more likely): With `srtNavT0 > srtPnLProjected`, the full `srtPnLProjected` is reversed so Junior is made whole, but the pooled remaining Senior is over-charged by the exiter's share and pushed below its own principal. 2. Near-total exit (cap binds): With `srtNavT0 < srtPnLProjected`, the reversal is capped, drives the remaining Senior base to zero, and the residual `srtPnLProjected − srtNavT0` falls on remaining Junior instead. The leak per exit equals the projected gain since the last reconciliation, larger when reconciliations are rare, and is repeatable by depositing, waiting, and withdrawing before each one.
Open the report`calculateRiskPremium()` calculates the Senior TVL ratio from the stored base NAVs without applying the valuation-loss adjustment. When `valuationPrice` is below `1e18`, the effective NAV calculation transfers value from Junior to Senior, increasing the Senior's effective share of total TVL. Because the risk-premium formula increases with the Senior TVL ratio, using the unadjusted ratio understates the risk premium during a depeg event. This can overpay Senior share holders and under-compensate Junior share holders precisely when juniors are absorbing additional valuation risk.
Open the reportA sub-threshold NAV move permanently desyncs the accounting's `nav` from the tranche totals, which eventually bricks every deposit, redemption, and share-price read on the vault. The `updateAccountingInner()` function overwrites `nav` with the strategy's reported value on every call, including the projection branch. Projection only shifts value between Senior and Junior and leaves the reserve untouched, so the committed tranche totals (`srtBaseNav + jrtBaseNav + reserveNav`) remain at the previous total. Reconciliation is the only path that rewrites those totals to match `nav`, and it is skipped whenever the realized move is below 1000 wei. That threshold exists to skip unnecessary reconciliations on small roundings. Each such move advances `nav` without touching the totals, leaving `nav` permanently above the tranches sum. The next move above the 1000 wei threshold triggers a reconciliation. The split starts from the old, lower totals and adds `pnl = navT1 - nav`, so it lands short of `navT1` by the earlier delta and reverts with `InvalidNavSplit`. Every reconciling path then reverts, including `totalAssets()`, which prices shares, so deposits and redemptions are blocked. `DiscreteAccounting` reconciles on any nonzero move and is therefore unaffected. This issue is specific to the `DYSAccounting` dust threshold.
Open the report`MidasStrategy.deposit()` calls `depositVault.depositInstant()` with `tokenAmount`, but the Midas vault computes `mintAmount` using a fee and the current `mTokenRate` conversion in `_convertUsdToMToken()`. As a result, minted mToken output is not a guaranteed one to one representation of USDC input. After the deposit, the strategy returns the USDC base asset value to the CDO, which increases NAV through `updateBalanceFlow()`. On the next `updateAccounting()` call, `totalAssets()` reflects true mToken value, and the gap is realized as a loss that is socialized by the loss waterfall.
Open the reportEvery other state mutating entry point reads the strategy value with `cdo.totalStrategyAssets(nav, _navAnchor())`. `setFloorRate()` passes `navTimestamp` instead. ```1117:1117:contracts/tranches/DYSAccounting.sol updateAccountingInner(cdo.totalStrategyAssets(nav, navTimestamp)); ``` When `useNavAtReconciliation` is true, `_navAnchor()` returns `lastReconciliation`, not `navTimestamp`, so this path uses the wrong time anchor and can detect or skip a reconciliation differently from every other entry point during the floor rate update. It is owner only and limited in scope, but it can misstate the NAV computed at that one update and contradicts the documented anchor semantics.
Open the reportSome `SharesCooldown` finalization paths call the external tranche redemption before removing the corresponding request from storage. If the selected output token invokes a receiver callback, the request owner can reenter `cancel()` while the request still exists and recover tranche shares after those shares have already been redeemed. When the selected request is the last array entry and another request remains, the outer execution can continue and remove a different request instead of reverting. This can pay the attacker both redemption assets and pooled tranche shares belonging to other cooldown requests, although the currently configured output tokens do not appear to provide such callbacks.
Open the reportThe function `MultiStrategy.totalAssets(navT0, timestamp)` treats any zero returned by a sub-strategy as a stale-oracle signal and returns the entire cached aggregate `navT0`. However, zero is also a valid fresh NAV when a sub-strategy has been fully withdrawn, rounded down, or suffered a complete loss. The protocol can consequently continue pricing deposits and withdrawals using assets that no longer exist instead of reconciling the loss. Existing holders may then withdraw against newly deposited capital, shifting an already-realized loss onto new users.
Open the report`MultiStrategy` stores a separate `accounting` reference even though its allocation calculations must use the accounting contract configured in its parent `StrataCDO`. The reference is independently mutable through `setAccounting()`, allowing it to diverge from the source used for tranche pricing and balance updates. If the two values differ, `IsolatedStrategy.imbalances()` calculates deficits and surpluses from unrelated NAV data. Deposits and rebalances may consequently be routed in the wrong direction.
Open the report`liquidAllocationFloor` can be initialized or updated to a value greater than `1e18`, even though the value represents a percentage of total NAV. Such a value produces a Junior target greater than total Junior and Senior NAV combined. The subsequent calculation of `totalNav - target` underflows and causes `imbalances()` to revert, which can block deposit routing and rebalancing operations.
Open the reportSub-strategies configured under a `MultiStrategy` treat that composite strategy as their `cdo`. Several strategy implementations call `cdo.isJrt(tranche)` to select tranche-specific cooldowns or configuration, but `MultiStrategy` does not expose that function. These calls revert when reached, which can block otherwise supported withdrawals from Midas, Saturn, Ethena, Neutrl, or Figure sub-strategies. Spark's local fallback avoids a revert but can apply the Senior cooldown to a Junior withdrawal instead.
Open the report`completeRebalance()` deposits the lesser of the available token balance and `pending.baseAssets` into the destination strategy. It nevertheless emits `RebalanceCompleted` with the original `pending.baseAssets`, even when the actual deposit was smaller. Off-chain accounting, monitoring, and automation that rely on the event can therefore record a larger completed rebalance than occurred on-chain.
Open the reportWhen `useNavAtReconciliation` is enabled, `_navAnchor()` returns `lastReconciliation`, the timestamp strategies use to detect new rewards. The `updateAccountingInner()` function advances `lastReconciliation` to `block.timestamp` whenever `navT1 != nav`, but an actual reconciliation only occurs when the move exceeds the 1000 wei threshold in `_shouldReconcile()`. A sub-threshold move therefore advances the anchor without reconciling, leaving it later than the true last reconciliation. Because the anchor now sits after the true last reconciliation, those strategies suppress detection of oracle rounds whose `updatedAt` is before the shifted anchor, delaying the next reconciliation by up to one oracle cycle and extending the projection window during which Junior funds the Senior `aprSrt` accretion with no true-up. The effect self-heals once a newer round, or all sub-strategies, update past the anchor, so a large pending round cannot be masked this way.
Open the report`IsolatedStrategy.setStrats()` can replace the configured Junior and Senior strategies while they still hold protocol assets. The function updates the strategy array and token-routing mappings but does not migrate or otherwise account for assets in the previous strategies. Those assets immediately disappear from `MultiStrategy.totalAssets()` and are no longer reachable through normal withdrawal or rebalance routing. A privileged configuration mistake can therefore create a large apparent NAV loss and leave protocol funds orphaned.
Open the report`maxDeposit()` limits how much Senior can deposit so that Junior stays at least `minimumJrtSrtRatioBuffer` (6%) of Senior. It does this by measuring Junior and Senior *before* the deposit and allowing Senior to grow up to the point where Junior would equal 6% of it. Normally this is exact. When the asset trades at par, a Senior deposit only adds to Senior and leaves Junior untouched — it moves one side of the ratio by exactly the deposited amount and nothing else. So the "before" snapshot predicts the "after" state perfectly, and depositing the maximum lands the ratio right on 6%. During a de-peg this breaks, because `calcEffectiveNav()` marks Senior back up to par by pulling value out of Junior, and the size of that transfer grows with Senior. Now a Senior deposit no longer touches just one side of the ratio, it touches both. The new Senior demands its own top-up, so Junior is drained further (the numerator drops), and the new Senior is itself marked up to par (the denominator grows by more than the deposit). The `maxDeposit()` cap does not consider both effects because it sized the deposit against the smaller, pre-deposit Senior without any effective nav markup. So it approves a deposit it believes lands at 6%, while the real result is a lower Junior over a higher Senior, pushing the effective ratio below the 6% buffer. The size of this gap scales with how deep the de-peg is, since a larger de-peg means a larger Senior top-up drawn from Junior. Under an extreme de-peg a single permitted deposit can therefore breach the ratio far more heavily. In the current implementation deposits should be paused once the valuation price is below `1e18`, however this problem becomes apparent if deposits are later reenabled without the peg returning to 1e18.
Open the reportEvery deferred rebalance appends an entry to `pendingRebalances`, while `Rebalancer.totalAssets()` loops over the complete array. A privileged operator can create many small pending rebalances by splitting an available deficit, causing the cost of accounting reads to grow without a fixed bound. Because `totalAssets()` is used by protocol accounting and user flows, a sufficiently large array can make those transactions exceed the block gas limit. The role restriction makes this primarily an operational and configuration risk rather than an unprivileged denial of service.
Open the reportWhen `MidasCooldownRequestImpl.request()` falls back to `redeemRequest()`, the proxy sets `pending = true` and stores only an expected amount, but it does not persist the external request identifier. If a Midas admin later cancels the redemption request (`RedemptionVault.rejectRequest()`, `MidasCooldownRequestImpl` has no status check or cancellation path to clear local state, so `pending` remains true and future `request()` calls revert.
Open the reportIn the current implementation of the `MidasCooldownImpl` proxy contract, a user is unable to recover their funds if the Midas protocol fulfils their withdrawal request with less than 90% of the expected value. In this scenario the `finalize()` function will always revert until at least 90% of the `requestedAmount` of `baseAsset` tokens are present in the contract. This means the only way for a user to successfully recover the funds in this scenario would be to transfer the necessary amount of `baseAssets` into the contract to reach the `requestedAmount` value, unlocking the `finalize()` function and allowing them to redeem the contract's entire `baseAsset` token balance.
Open the reportIn `MidasStrategy.deposit()`, the `minReceiveAmount` for `depositVault.depositInstant()` is hardcoded to `0`. Similarly, in `MidasCooldownRequestImpl.request()`, the `minReceiveAmount` for `redemptionVault.redeemInstant()` is also `0`. While Midas uses oracle-based pricing (reducing traditional AMM sandwich risk), a rate change between submission and execution, an oracle update mid-block, or a fee change by the Midas admin could cause the strategy to receive significantly fewer tokens than expected with no revert threshold.
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="strata-1"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us