Quantstamp reviewed Tapir (tapir protocol) on 6 Jan 2026: 9 findings, 0 critical; all marked fixed.
Report hosted on certificate.quantstamp.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 3 |
| Medium | 0 | 0 | 3 |
| Low | 0 | 0 | 3 |
The logic of the `sweepFeesToTreasury()` function calculates how many of the `ASSET` tokens held in the contract can be transferred to the treasury as earned fees. However, due to an error in how this amount is calculated, `feeAmount` will be half of the total `ASSET` tokens in the contract. This is because `dpSupply` and `ybSupply` are expected to be 50% of the contract's total `ASSET` balance. As this function is permissionless and can be called at state in the protocols lifecycle, there is a significant risk of griefing where funds can be repeatedly forced to the treasury, leaving the protocol under-collateralized.
In the `resolvePtrw()` function, if the attempt to redeem PT tokens from Pendle fails (no tokens are redeemed), a manual price resolve is required. However, in the current implementation, the `resolvePtrw()` function immediately reverts after setting `forceManualResolve = true`, rewinding all state transitions made during the function call, including the setting of the `forceManualResolve` boolean. This means that in scenarios where a manual resolution is necessary, if the `pendlePT` balance of the `TapirPtrwOracle` contract is non-zero it will not be possible to execute a manual resolution, leaving the oracle contract in a deadlocked state where the oracle contract cannot write price data to the `DepegPool` contract.
The `DepegPool` contract has a `MIN_PRICE` that the underlying asset must be when the `TapirOracle` provides the final price data via the `updatePriceData()` function. However, if there is a significant depeg event there is no guarantee that the price of the underlying asset will be greater than `MIN_PRICE`. In this scenario, the `DepegPool` contract will not be able to progress into the `REDEMPTION` stage, preventing users from withdrawing their funds.
In `TapirOracle.checkpoint()`, checks are made to ensure the price provided by an oracle has been updated within a specific `maxPriceStaleness`. However, different price feeds will have different update tolerances (heartbeats), where if the price has not moved outside of the accepted tolerance, the oracle will not be updated unnecessarily until the heartbeat time. This could lead to the `checkpoint()` function mistakenly considering a price stale while it is still actually valid. Alternatively, if the `maxPriceStaleness` was too large for a specific price source, a stale price might be considered valid by the Tapir oracle.
The `minPriceAge` parameter is mutable via `setMinPriceAge()` without state guards. The `resolvePriceDepeg()` function requires `block.timestamp - finalPriceData.timestamp >= minPriceAge` before allowing resolution. When an admin increases `minPriceAge` (up to MAX_DURATION = 30 days) while the pool is in RESOLUTION state, the function reverts with `PriceDataTooRecent`, indefinitely blocking progression to REDEMPTIONS and effectively locking user funds. Unlike HIGH-01 (backwards transition), this freezes the state machine in RESOLUTION.
When the `ASSET` token of a `DepegPool` instance is a yield bearing asset users are likely to face a `successFee` upon redemption which takes a percentage fee of the users redeemable amount based on the price increase of the underlying asset from the `startingPrice` when the pool was deployed. For users who deposit towards the end of the pools `activeDuration` they will still be forced to pay the full `successFee` even though the majority of the value accrual occurred before the user's assets were added to the pool. This potential risk is not clearly outlined to the user who may end up with less `ASSET` tokens than they originally deposited due to the `successFee`.
Pools used as price oracles should document the dual-fee system: ```solidity uint24 public immutable override fee; // Pool identifier uint24 public override dynamicFee; // Actual swap fee ``` For Oracle Integrators: - Use `fee()` for pool identification and tick spacing - Use `dynamicFee()` for actual swap cost calculations - TWAP calculations are unaffected (based on price, not fees)
The protocol expected a strict linear progression from the state machine as follows: 1 (`STATE_ACTIVE`) → 2 (`STATE_COOLDOWN`) → 3 (`STATE_RESOLUTION`) → 4 (`STATE_REDEMPTIONS`). However, because the protocol's admin is able to change the `cooldownDuration` at any time, the project could theoretically move from a later state back to `STATE_COOLDOWN`. Due to how the current state is assessed in the `getState()` function, it is possible to end up with a transition from `STATE_RESOLUTION` → `STATE_COOLDOWN `(3 → 2) and from `STATE_REDEMPTIONS` → `STATE_COOLDOWN` (4 → 2).
The `DepegPool` contract packs its storage variables for a more efficient storage layout. However, it also unnecessarily packs variables marked as `constant` and `immutable`. This is unnecessary as both constant and immutable variables in solidity are compiled directly into the contract's bytecode rather than being held in the contract's storage onchain.
The report names a repository or files. Open the report to see its scope.
236 projects reviewed · 332 reports · latest Oct 2026
See Quantstamp’s auditsSomething 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