As of 30 Oct 2026, Taler Finance has 1 audit report on Trustblock, by Quantstamp, dated 21 Apr 2026. That report lists 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 |
|---|---|---|---|---|---|
| Taler | 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 | 0 | 0 | 2 |
| Low | 0 | 3 | 11 |
The fee system in `FeeModule` uses a two-path accounting model for management fees. During accrual in `_accrueFees()`, the management fee accumulator `mgmtSharesPerBps` advances and `totalPendingFeeShares` is incremented by the floor-divided share count for that individual accrual. Every accrual adds an increment by a floored share count. When `claimFees()` later calls `_settleEntry()`, it computes the owed shares from the full accumulated delta since the entry's last checkpoint as a single floor division over the combined delta. However, by the fundamental property of floor division, `floor(a) + floor(b) <= floor(a + b)`. This means the sum of per-call pending increments accumulated across multiple accruals can be less than the single-shot settlement amount computed in `_settleEntry()`. When the fractional remainders of the individual accrual divisions sum to at least `ACCUMULATOR_PRECISION`, the settlement exceeds `totalPendingFeeShares` by at least 1 wei. The subtraction `totalPendingFeeShares -= shares` in `claimFees()` then underflows and reverts. This occurs during normal vault operations with no attacker required. Any two deposits, withdrawals, or other operations that trigger `_accrueFees()` on different blocks without a `claimFees()` call in between can create the 1-wei drift. The revert blocks the entire `claimFees()` call across all entries, as well as `removeFeeEntry()` and `removeFeeEntryAsAdmin()`. The DoS is recoverable without a contract upgrade through a careful settle-between-accruals admin procedure, but this requires multiple cycles and is non-trivial. User funds remain safe as fee accounting is isolated from share and asset accounting.
Open the reportThe `FeeModule` maintains a global `highWaterMarkNavPerShare` to prevent double-charging performance fees on the same NAV gain. When all performance fee entries are removed via `_removeFeeEntry()`, the code resets the high-water mark to zero when `totalPerformanceFeeBps` reaches zero. This allows `_initializeFeeTimingState()` to properly re-initialize the high-water mark at the current `assetsPerShare()` when a new performance fee entry is later added. However, when `updateFeeEntry()` is used to set an entry's performance fee to 0 bps instead of removing the entry, `totalPerformanceFeeBps` drops to zero but the high-water mark is not reset. The HWM reset logic exists only in `_removeFeeEntry()` and is absent from `_updateFeeEntry()`. When a new performance fee entry is subsequently added via `addFeeEntry()`, `_initializeFeeTimingState()` checks whether `highWaterMarkNavPerShare` equals zero and skips re-initialization because the stale value persists. A related discrepancy exists on the `updateFeeEntry()` path. When `_updateFeeEntry()` is used to set an entry's performance fee to 0 bps instead of removing it, `totalPerformanceFeeBps` drops to zero but the HWM is not reset because the reset logic only exists in `_removeFeeEntry()`. The stale HWM then persists, and when a new performance fee entry is later added via `addFeeEntry()`, `_initializeFeeTimingState()` skips re-initialization because `highWaterMarkNavPerShare` is nonzero. Depending on NAV movement relative to the stale mark, fee recipients may lose legitimately earned performance fees (if the new entry must exceed an old HWM that is above the intended starting point), or depositors may be overcharged (if the stale HWM sits below the price at which the new entry was meant to begin tracking).
Open the reportIn the `AsyncRedemptionModule`, `requestRedemptionWithPermit()` calls `IERC20Permit.permit()` directly without a try/catch wrapper. This differs from the `Vault._tryPermit()` which gracefully handles permit failures using try/catch. If a user's permit signature is front-run and consumed by a third party before their transaction executes, the `requestRedemptionWithPermit()` call will revert, forcing the user to fall back to a separate `approve()` + `requestRedemption()` two-transaction flow.
Open the reportIn both gateway execution paths, the event `ExecutionFromModuleFailure` is effectively unreachable under the current vault implementation. `ExecutionGateway.execTransactionFromModule()` performs a low level call to `Vault.executeAction()` and immediately bubbles the revert data if that call fails. `ExecutionGateway.execTransactionFromModuleReturnData()` invokes `Vault.executeAction()` through a Solidity external call, which also bubbles reverts by default. Inside `Vault.executeAction()`, the downstream call result is checked and the function reverts with `Vault_ActionFailed()` whenever the action result is false, instead of returning `(false, returnData)`. As a result, both gateway functions proceed to event emission only on successful execution, making `ExecutionFromModuleFailure` a dead monitoring signal for offchain services. Integrators that rely on that event for failure detection will miss failed executions and must parse revert traces instead.
Open the report`VaultFactory.deployVaultSystem()` accepts assets with up to 77 decimals. Because share decimals equal `assetDecimals + 2`, a 77 decimal asset produces a 79 decimal share token. Both `Accounting.assetsPerShare()` and `AsyncRedemptionModule._convertToAssets()` compute `10 ** shareDecimals`, which overflows `uint256` at 79 decimals. Deposits still succeed because their conversion path only uses `10 ** DECIMALS_OFFSET` (where offset is two), so the overflow surfaces later when settlement calls `assetsPerShare()`, permanently blocking epoch settlement and user claims.
Open the reportIn the `Accounting` contract, `removePosition()` calls `position.adapter.calculateNav()` and requires the result to equal zero before allowing removal. If an adapter becomes non-functional (e.g., reverts due to a broken oracle dependency, a paused external protocol, or a buggy upgrade), the `calculateNav()` call will revert, making it impossible to remove the position through the normal path. Since `totalAssets()` also iterates all positions and calls `calculateNav()`, a bricked adapter will additionally DoS deposits, redemptions, and fee accrual until the adapter is restored or an upgrade is performed.
Open the reportThe management fee accrual path in `_accrueFees()` computes two values on each call: the accumulator delta `mgmtAccumDelta` which is unconditionally added to `mgmtSharesPerBps`, and the total management shares `totalMgmtShares` derived by multiplying the delta back by `totalMgmtBps` divided by `ACCUMULATOR_PRECISION`. The `totalPendingFeeShares` tracker is only incremented when `totalMgmtShares` is non-zero. However, when `mgmtAccumDelta * totalMgmtBps` is less than `ACCUMULATOR_PRECISION`, the floor division produces zero shares and `totalPendingFeeShares` is not incremented while the accumulator continues to advance. Over many such zero-rounding calls, the accumulated delta grows large enough that `_settleEntry()` produces non-zero shares that were never tracked in `totalPendingFeeShares`. The subtraction in `claimFees()` then underflows and reverts. This requires an active attacker calling `accrueFees()` at high frequency on vaults with low supply and low-decimal underlying tokens such as WBTC or USDC. The thresholds are restrictive: a WBTC vault at 200 bps must hold under approximately 9,450 dollars worth of assets for the zero-rounding condition to trigger on per-second accruals. For 18-decimal tokens the threshold is below one billionth of a dollar, making them immune. The attack cost in gas significantly exceeds the value of fees denied, making it economically irrational.
Open the reportThe protocol currently targets Ethereum L1, where Chainlink feeds update independently and no sequencer exists. However, the team has stated the code should be compatible with L2 networks. When the sequencer goes down and later recovers, it replays queued transactions against oracle prices that may not have been updated during the outage. Chainlink provides [L2 Sequencer Uptime Feeds](https://docs.chain.link/data-feeds/l2-sequencer-feeds) specifically for this scenario. Neither `OracleMiddleware` nor any of the NAV adapters check the sequencer uptime feed or enforce a grace period after the sequencer restarts. If the protocol is deployed to an L2 without adding this check, the adapters could read stale prices immediately after a sequencer recovery, before Chainlink feeds have had the opportunity to update. This can lead to incorrect NAV calculations during the post recovery window, potentially allowing deposits or redemption settlements at mispriced values.
Open the report`Vault.executeAction()` supports sending native tokens via `to.call{value: value}(data)`, but there is no way for native tokens to reach the vault. The `ExecutionGateway` forwards execution requests to the vault through two functions, and neither passes `msg.value` along. Additionally, `Vault` does not declare a `receive()` or `fallback()` function. Together, these omissions make the `value` parameter in `executeAction()` dead code. This prevents the vault from interacting with any external protocol that requires native token payment as part of a function call.
Open the reportThe NAV slippage protection in `Vault.executeAction()` compares the NAV before and after each individual execution. After each execution, the NAV cache is updated to `navAfter`. This means the slippage baseline resets with every call. An executor can chain multiple actions, each reducing the NAV by just under the tolerance limit, and the cumulative reduction compounds well beyond what the vault owner likely intended when setting the tolerance.
Open the reportEach NAV adapter (`MorphoBlueNavAdapter`, `AaveV3NavAdapter`, and `ERC20OracleNavAdapter`) stores a single `maxStaleness` value that is used as the staleness check for every price feed the adapter queries. Different Chainlink feeds have different heartbeats depending on the asset pair and network. For example, ETH/USD on Ethereum mainnet has a one hour heartbeat while USDC/USD has a 24 hour heartbeat. A single `maxStaleness` cannot correctly accommodate both. This is most visible in `MorphoBlueNavAdapter`, where a single `calculateNav()` call can query two distinct feeds in the same transaction: one for `loanToken` and one for `collateralToken`. Both staleness checks in `_priceInUnderlying()` use the same `$.maxStaleness`. If the threshold is set to the longer heartbeat (e.g., 24 hours for a stablecoin feed), a shorter heartbeat feed (e.g., one hour for an ETH feed) can silently return stale data without reverting. Conversely, if it is set to the shorter heartbeat, legitimate updates on the slower feed will be rejected, causing `calculateNav()` to revert and blocking vault operations such as deposits, redemption settlement, and epoch closes. The same pattern applies to `AaveV3NavAdapter` and `ERC20OracleNavAdapter`, which are called with different tokens via their context parameter but apply the same adapter level staleness threshold to every feed.
Open the reportWhen a fee entry is updated to zero management and zero performance bps via `updateFeeEntry()`, `lastFeeAccrualTimestamp` is not reset. `_accrueFees()` returns early only when both `totalManagementFeeBps` and `totalPerformanceFeeBps` are zero, so during that window the timestamp does not advance. If only management is zero but performance remains nonzero, accrual still runs and the timestamp updates, so this issue does not apply. When fees are later re-enabled through another `updateFeeEntry()` call, the next accrual computes elapsed time from the stale timestamp. The resulting overcharge is a management fee effect, because management accrual scales with elapsed time. Performance fees are NAV and high water mark based, not a simple catch up over the gap in the same way.
Open the reportThe `FeeModule`'s `_validateFeeEntry()` function validates new and updated fee entries. The validation check for the recipient address is conditional: it only requires a non-zero recipient when at least one of `managementFeeBps` or `performanceFeeBps` is greater than zero. When both bps values are zero, the validation passes regardless of the recipient address, allowing entries with `recipient` set to `address(0)`. However, when an existing entry with accrued shares is updated to `0/0/address(0)` via `updateFeeEntry()`, the `_settleEntry()` call during the update moves accumulated shares into `entry.accruedShares`. The entry now holds a zero-fee configuration with a null recipient but non-zero `accruedShares`. When `claimFees()` iterates all entries, it attempts to mint fee shares to `address(0)` for this entry, which triggers a revert from OpenZeppelin's `ERC20._mint()` with `ERC20InvalidReceiver(address(0))`. This could lead to a complete denial of service on the fee claiming system. Since `claimFees()` processes all entries in a single transaction, the revert from the broken entry propagates and blocks claiming for all entries. The issue requires the `FEE_MANAGER` role to trigger, but a manager might reasonably expect that setting an entry to 0/0 effectively disables it rather than creating a permanent claim blocker.
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="taler-finance"></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