#44064 [SC-Medium] Dispatcher incorrect validation causes principal tokens to be stuck in inheriting contract allowing attacker to steal user funds

Submitted on Apr 16th 2025 at 14:58:45 UTC by @io10 for Audit Comp | Spectra Finance

  • Report ID: #44064

  • Report Type: Smart Contract

  • Report severity: Medium

  • Target: https://github.com/immunefi-team/Spectra-Audit-Competition/blob/main/src/router/Dispatcher.sol

  • Impacts:

    • Permanent freezing of unclaimed yield

    • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Brief/Intro

The Dispatcher::_dispatch function in Spectra is responsible for routing user commands to various protocol operations, such as transfers, redemptions, and pool interactions. A bug in the REDEEM_PT_FOR_ASSET command causes the dispatcher to incorrectly prevent PT redemptions if the contract lacks a balance of YT — even after maturity, when YT is no longer required. This causes user funds to become stuck or misrouted, leading to potential loss or theft.

Vulnerability Details

Dispatcher::_dispatch is a command that allows any user to execute certain commands that relate to token transfers, pool creation and interactions with pt, ibt and yt tokens. Contracts can inherit the functionality of the dispatcher to allow function routing to allowed contracts.

The vulnerability in Dispatcher::_dispatch is in the following code block:

PrincipalToken::redeem works as follows:

_beforeRedeem performs some checks before allowing a user redeem assets for pt. These checks include checking if the principal token is at or past maturity, in this case, it only updates the rates at expiry. If maturity hasnt been reached, then the user is required to burn an equivalent amount of yt tokens to redeem their assets. This is consistent with the natspec at the start of the principal token which says:

" The shares of the vaults are composed by PT/YT pairs. These are always minted at same times and amounts upon deposits.Until expiry burning shares necessitates to burn both tokens. At expiry, burning PTs is sufficient. This check is making sure that the contract has enough yt to redeem the shares which is not necessary for pt's redemption at expiry" .

Dispatcher::_dispatch in the code block above, does not check if the principal token is at maturity or not and simply allocates shares based on how much yt the user has sent to the contract. As a result, users who have sold/transferred their yt's will not be able to redeem pt via the dispatcher as it will always calculate the amount of shares to redeem as 0. Any user who attempts to redeem PT for asset via the dispatcher will have their principal tokens stuck in the contract which allows an attacker to steal the user's principal tokens by calling the transfer command via the dispatcher.

Impact Details

This vulnerability creates a situation where any user attempting to redeem PT for assets via the dispatcher will be blocked from redeeming if they no longer hold the corresponding YT tokens — even if the PT has reached maturity. This occurs because the dispatcher limits the redeemable share amount to the contract's balance of YT, which is only necessary pre-maturity. After maturity, the PT can be redeemed independently.

As a result, users can mistakenly transfer their PTs to the dispatcher and trigger a redeem operation, expecting to receive underlying assets. Instead, the dispatcher will attempt to redeem 0 shares (based on the YT balance check), and the user will receive nothing, while their PTs remain stuck in the dispatcher.

Consequences: Funds Stuck (Denial of Redemption): Users' PT tokens become irretrievable if they mistakenly redeem via the dispatcher post-maturity, leading to permanent loss of access to their principal assets.

Theft via Malicious Redemption: An attacker can monitor for such stuck PTs and, by using a subsequent dispatcher TRANSFER_ERC20 command, redirect those PTs to themselves or front-run users to steal trapped PTs.

Loss of User Funds: This could result in significant monetary loss, especially for high-value deposits at maturity.

Proof of Concept

Proof of Concept

This test was run in Router.t.sol using the router as the child contract for the dispatcher to display the vulnerability

To see, that 0 underlying asset were transferred to testUser and the user's principal tokens are still in the router contract, uncomment the assert statements

Was this helpful?