xanoutas/solidity-security-findings huggingface dataset:
IBT rate manipulation exploits rounding, inflates user withdrawals.
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L879-L894
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/libraries/PrincipalTokenUtil.sol#L55


# Vulnerability details


## Impact
Manipulating IBT rates near expiry or during negative yield periods allows attackers to exploit decimal conversion rounding and withdraw more assets than they deposited. This damages user funds and capital efficiency.

The core issue here is that the IBT rate directly impacts the PT rate calculation. The [_updatePTandIBTRates()](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L879-L894) function recalculates PT rate based on current vs previous IBT rate

```solidity
function _updatePTandIBTRates() internal returns (uint256 _ptRate, uint256 _ibtRate) {
    if (block.timestamp >= expiry) {
        if (!ratesAtExpiryStored) {
            storeRatesAtExpiry();
        }
    }
    (_ptRate, _ibtRate) = _getPTandIBTRates(false);
    if (block.timestamp < expiry) {
        if (_ibtRate != ibtRate) {
            ibtRate = _ibtRate;
        }
        if (_ptRate != ptRate) {
            ptRate = _ptRate;
        }
    }
}
```

As we can see on line, if IBT rate decreases, it can round the division in a way that favors the user over the protocol.

This allows a malicious actor to manipulate IBT rate via flashloans right before user redemptions and exploit the imprecise PT rate calculation. For example, briefly borrow 1B IBT tokens, redeem them from the underlying protocol to significantly reduce IBT rate, then return loan. Users redemptions get credited extra assets due to inflated PT rate.

This could realistically happen as an attack vector.

**Mitigations:**

- Explicitly round down PT rate recalculation on IBT rate decreases
- Requirements to restrict IBT rate manipulation from external sources 

## Negative Yields

The main issue here is [_computeYield()](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/libraries/PrincipalTokenUtil.sol#L55) handles negative yield scenarios by doing multiple conversions between assets, IBT amounts, token rates, and decimals. For example:

```solidity 
function _computeYield(
  // args
) external view returns (uint256) {

  if (_ibtRate >= _oldIBTRate) {
    // positive yield
  } else {  
    // negative yield
    
    uint256 actualNegativeYieldAssets = convertSharesToAssets(
        userYTBalance, _oldPTRate, Rounding.Floor);

    uint256 expectedNegativeYieldAssets = convertIBTtoAssets(
        ibtOfPT, _oldIBTRate - _newIBTRate, Rounding.Ceil);
    
    // ... more conversions ...

  }

}
```

The problem is these repeated conversions with differing rounding rules can ultimately credit users more assets than they should receive. For example, a user redeeming 100 shares sees the `_com
Math precision issues risk unfair user profit margins.
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/libraries/RayMath.sol#L20-L25
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L430-L433
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L171-L173
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L680-L693


# Vulnerability details


## Impact
The RayMath library handles conversions between the high-precision 27 decimal fixed point Ray unit and various target token precisions (like 6 to 18 decimals). This complexity introduces risk of overflows or benefiting users from rounding errors.

For example, the [fromRay](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/libraries/RayMath.sol#L20-L25) function used throughout PrincipalToken contract

```solidity
function fromRay(uint256 _a, uint256 _decimals) internal pure returns (uint256 b) {
  uint256 decimals_ratio = 10**27 - _decimals); 
  assembly {
     b := div(_a, decimals_ratio)
  }
}
```

This divides the Ray amount by the ratio between 27 and target decimals. The truncating integer division could allow users to unfairly profit if additional decimals are discarded.

**Scenario:** 
1. User deposits 100 DAI (18 decimals) which converts to 100 * 10^27 Ray 
2. Later withdraws all shares, converting Ray back to DAI
3. If ratio math is incorrect, could get back 100.00000001 DAI instead of 100

This seems unlikely given the 27 decimal buffer, but overflow risks still exist. Alternatives like Decimal.sol reduce risk but have tradeoffs.

**PrincipalToken**

When previewing or converting assets deposited, small rounding diffs could advantage users:  In [previewDeposit](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L430-L433) & [deposit](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L171-L173)

```solidity
function previewDeposit(uint256 assets) public view override returns (uint256) {
    uint256 ibts = IERC4626(ibt).previewDeposit(assets);
    return _previewDepositIBT(ibts);
}

    function deposit(uint256 assets, address receiver) external override returns (uint256 shares) {
        shares = deposit(assets, receiver, receiver);
    }
```

If `shares` from [convertIBTsToShares](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L680-L693) is even 1 wei higher on actual deposit, users earn more yield. This seems unlikely to be manipulated given sizable 27 decimal buffers but still concerning.

## Proof of Concept
1. User calls `previewDeposit` with 100 DAI, getting back 1000 PT shares 
2. User then calls `deposit
Negative yield exploit allows PT rate manipulation, asset extraction.
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L879-L894
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L879-L894


# Vulnerability details


## Impact
The Spectra protocol handles negative yield events by depegging the Principal Token (PT) rate from the Interest Bearing Token (IBT) rate. This means if there is a negative yield, the PT rate can decrease independently while the IBT rate decreases. 

The [_updatePTandIBTRates](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L879-L894) function updates the PT rate if a negative yield event happens. It uses the formula:

If currentIBTRate < ibtRate (negative yield), the PT rate is adjusted downwards.

*The Vulnerability:*
An attacker could try to artificially trigger high negative yields right before withdrawing. This would sharply drop the PT rate, allowing the attacker to redeem much more assets for their PT.

*Exploit Scenario:*

1. Attacker deposits 100 DAI into a new PT contract, minting 100 PT at a rate of 1 PT : 1 DAI. 
2. Through flashloans, the attacker repeatedly borrows the IBT holdings of the PT contract. This could drain enough assets to cause a -90% yield. 
3. The IBT rate drops to 0.1 DAI, forcing the PT rate to depeg from 1 to 0.1 PT : 1 DAI.  
4. The attacker quickly redeems their 100 PT for 100 * 0.1 = 10 DAI.
5. The attacker has now exploited the depeg to redeem 10x more assets than they deposited.

*Occurrence Likelihood:* 
This attack requires manipulation of negative yields, which may be possible through flashloans. It also relies on timely redemption. The likelihood depends on the ease of manipulating yields for an IBT asset.

[In PrincipalToken.sol::_updatePTandIBTRates](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L879-L894)
```solidity
function _updatePTandIBTRates() internal returns (uint256 _ptRate, uint256 _ibtRate) {
    if (block.timestamp >= expiry) {
        if (!ratesAtExpiryStored) {
            storeRatesAtExpiry();
        }
    }
    (_ptRate, _ibtRate) = _getPTandIBTRates(false);
    if (block.timestamp < expiry) {
        if (_ibtRate != ibtRate) {
            ibtRate = _ibtRate;
        }
        if (_ptRate != ptRate) {
            ptRate = _ptRate;
        }
    }
}
```
By targeting this rate update logic, an attacker could profit from negative yield depegs. This break assumptions around PT rate only decreasing to account for negative yields.

The root is delegating the PT rate calculation to user controlled variables. A potential mitigation is to cap the maximum rate change per block. This would limit manipulation.

## Proof of Concept
An attacker could exploit negative yields to extract 10x or more assets than they deposited. This breaks the as
Reentrancy allows double minting, inflating token supply, extracting value.
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L750-L769


# Vulnerability details


## Impact
`_depositIBT` function handles depositing IBT into the Principal Token vault, minting corresponding PT and YT. [In PrincipalToken.sol::_depositIBT
](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L750-L769)
```solidity
function _depositIBT(
    uint256 _ibts,
    address _ptReceiver,
    address _ytReceiver
) internal notExpired nonReentrant whenNotPaused returns (uint256 shares) {
    
    // Update user yield  <@
    updateYield(_ytReceiver);

    // Compute tokenization fee <@
    uint256 tokenizationFee = PrincipalTokenUtil._computeTokenizationFee(
        _ibts,
        address(this),
        registry
    );

    // Update total fees  <@
    _updateFees(tokenizationFee);

    // Mint new shares    <@
    shares = _convertIBTsToShares(_ibts - tokenizationFee, false);
    if (shares == 0) {
        revert RateError();
    }

    // Mint tokens        <@
    _mint(_ptReceiver, shares);
    emit Mint(msg.sender, _ptReceiver, shares);
    IYieldToken(yt).mint(_ytReceiver, shares);
}
```
There is a risk of reentrancy here between updating yields and minting new shares. 

A malicious contract could implement `IYieldToken` and reenter `_depositIBT` before shares are minted. This could enable inflating the `_ibts` balance used to mint shares by calling `_convertIBTsToShares` again before shares are minted.

By reentering, duplicate PT and YT minting would occur while fees only paid once. This breaks share supply invariants.

**Root cause** - Shares minting occurs after potential reentrancy trigger in `updateYield`.

User extracts extra protocol value by inflating share supply via reentrancy. Breaks key PT/YT supply equality invariant.

## Proof of Concept
This reentrancy vulnerability enables attackers to artificially inflate the Principal Token and Yield Token supply. By reentering before shares are minted, an attacker can double mint tokens for only a single deposit amount. This breaks the key token supply ratio invariants in the protocol.

Attackers can exploit this to extract additional value from the protocol by minting free shares that they can later redeem or withdraw against.

1. Attacker deploys a malicious contract implementing `IYieldToken`

2. Attacker calls `_depositIBT` with 100 IBT deposit

3. On `updateYield` call, malicious contract reenters `_depositIBT`

4. In reentered call, `_ibts` balance still reflects original 100 deposit 

5. Tokenization fee only deducted once across two calls  

6. `_convertIBTsToShares` uses inflated `_ibts` balance to double mint shares

7. Attacker burns 100 extra PT + YT they shouldn't have been able to mint

Performing the token share conversion and minting after `updateYield`. This enables reentering at `updateYield` and inflating the `_ibts` b
Overflows in calculations truncate user balances, causing substantial losses.
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/libraries/RayMath.sol#L20-L25
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/libraries/PrincipalTokenUtil.sol#L75


# Vulnerability details

## Vulnerability Details
The `fromRay` function can cause overflows, resulting in substantial loss of user balances. For large token amounts, the conversion can truncate the balance to 0 or a low number. This directly harms users by incorrectly deducting their owed interest and token holdings. Over time, the cumulative loss could be significant.

In the `fromRay` function of RayMath.sol, there is a potential unsafe cast when converting the rayUnit amount to the target decimal representation: [In src/libraries/RayMath.sol::FromRay](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/libraries/RayMath.sol#L20-L25)

```solidity
function fromRay(uint256 _a, uint256 _decimals) internal pure returns (uint256 b) {
    uint256 decimals_ratio = 10 ** (27 - _decimals);
    assembly {
        b := div(_a, decimals_ratio)
    }
}
```

This could cause an overflow if `_a` is very large. For example: 

```solidity
_a = 115792089237316195423570985008687907853269984665640564039457584007913129639935 
_decimals = 18

decimalsRatio = 10 ** (27 - 18) = 1000000000000000000

b = _a / decimalsRatio = overflow!
```

This overflow would incorrectly truncate the user's balance.

**Mitigation**: Check for overflows and throw errors instead of casting. Require `_a` fits in 256 bits.

**Yield Calculation Rounding** 

In `_computeYield`, there is a division when converting interest earned to shares: [Line 55](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/libraries/PrincipalTokenUtil.sol#L75)

```solidity 
newYieldInIBTRay = ibtOfPTInRay.mulDiv(_ibtRate - _oldIBTRate, _ibtRate);
```

Standard solidity `/` rounds down, deducting small amounts from user balances each accrual. 

Over months of compounding, this always rounding down could deduct a meaningful amount of interest from the user.

**Mitigation**: Use a rounding up division method instead.

**Incorrect Compound Ordering**

Currently interest accrues and then fees are deducted. This means fees deduct interest owed to users rather than just principal:

```solidity
calculateInterestOwed(user)
deductFees(interestOwed) 
```

**Mitigation**: Flip order to deduct fees first.

**No Persistence of Interest Owed**

The `_computeYield` function calculates interest owed each time but does not accumulate or persist it anywhere.

This means previously owed interest that has not yet been credited can be "lost" if not accounted for.

**Mitigation**: Track interest owed for each user over time. Credit accumulated interest first before further accrual.

## Proof of Concept
The unsafe casting in the `fromRay` function can cause overflows, resulting
Unverified IBT vault rate leads to PT inflation, value extraction.
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L901-L914


# Vulnerability details


## Description
The `_ibtRate` is updated in `_updatePTandIBTRates()`. This relies on calling `_getCurrentPTandIBTRates()` which gets the latest rate from the IBT vault via `IERC4626(ibt).previewRedeem(ibtUnit)`. 

There is a risk here that if the IBT vault itself could be manipulated, it could alter the `_ibtRate` in an unauthorized way. For example, if the IBT vault owner colluded with a user and artificially decreased the IBT rate returned from `previewRedeem`, it could trigger PT share supply inflation when new deposits come in at the lower `_ibtRate`. 

This could allow users to extract extra value. Specifically, in `_depositIBT()`, new shares are minted based on `_ibtRate`. If it is reduced through manipulation, more shares are minted per IBT deposited. When users later redeem at the true higher IBT rate, they get excess assets.

**Root**: fetching `_ibtRate` directly from an unverified source (the IBT vault).

## Impact
PT share supply inflation; extraction of value.

## Proof of Concept
This vulnerability allows attackers to extract excess value from the protocol by redeeming PT shares for more assets than they deposited. Specifically, when the IBT rate increases, the PT rate fails to decrease proportionally. This means PT shares can be redeemed for more assets than they should based on the vault's yields.

If exploited, this would lead to loss of user funds and instability of the Spectra protocol.

1. Attacker deposits 100 DAI into a Spectra PT vault with 1 DAI = 1 PT share.

2. The PT vault holds the DAI in an IBT vault with current IBT rate of 1 IBT = 1 DAI. 

3. Attacker receives 100 PT shares. 

4. The IBT vault yield increases, raising the IBT rate to 1 IBT = 1.1 DAI.

5. However, the PT contract does not update the PT rate accordingly. 

6. Attacker redeems their 100 PT shares and receives 110 DAI, more than the 100 DAI deposited.

7. By redeeming at an outdated PT rate, the attacker extracted 10 DAI of excess value.

In [PrincipalToken.sol::_getCurrentPTandIBTRates](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L901-L914)

```solidity
function _getCurrentPTandIBTRates(bool roundUpPTRate) internal view returns (uint256, uint256) {
    
    // Get latest IBT rate   <@
    uint256 currentIBTRate = IERC4626(ibt).previewRedeem(ibtUnit).toRay(_assetDecimals);
    if (IERC4626(ibt).totalAssets() == 0 && IERC4626(ibt).totalSupply() != 0) {
        currentIBTRate = 0;
    }

    // Only decrease PT rate if IBT rate decreased  <@
    uint256 currentPTRate = currentIBTRate < ibtRate
        ? ptRate.mulDiv(
            currentIBTRate,
            ibtRate,
            roundUpPTRate ? Math.Rounding.Ceil : Math.Rounding.Floor
        )
        : ptRate;
    return (currentPTRate, currentIBTRate);
Lack of protection against repeated `_updatePTandIBTRates` calls allows attackers to arbitrarily drain PT rate.
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L879-L894


# Vulnerability details

## Impact
`_updatePTandIBTRates` method that decreases the PT rate has no protection against unbounded/repeated calls. An attacker could manipulate the parameters to `_computeYield` to generate arbitrary negative yield amounts. They could then call `_updatePTandIBTRates` repeatedly to drain the PT rate to 0.

For example, they could:

1. Deposit tokens into the protocol to mint PT shares.

2. Borrow a flash loan of IBT tokens.

3. Use the flash loaned IBT to manipulate the IBT rate oracle downwards. 

4. Call `_computeYield` with the manipulated rate to generate a large negative yield amount.

5. Call `_updatePTandIBTRates` to decrease PT rate based on the false negative yield.

6. Return flash loan and repeat process in a loop draining PT rate to 0. 

7. Redeem PT shares for underlying assets while PT rate is 0.

This allows an attacker to arbitrarily drain value from the protocol by exploiting unbounded `_updatePTandIBTRates` calls.

The reason is that protection against repeated `_updatePTandIBTRates` calls in a single transaction.

The attacker can drain PT rate to 0 via unbounded manipulation.

The lack of protection against repeated `_updatePTandIBTRates` calls allows attackers to arbitrarily drain the PT rate to 0. This could enable attackers to extract full collateral value from the protocol after manipulating the rate.

## Proof of Concept
The `_updatePTandIBTRates` method decreases the `ptRate` based on detected negative yield. However, there is no limit on how many times it can be called in a single transaction. The issue is `_updatePTandIBTRates` lacks a counter or limit on repeated calls: [File: src/tokens/PrincipalToken.sol::_updatePTandIBTRates](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L879-L894)

```solidity
function _updatePTandIBTRates() internal returns (uint256 _ptRate, uint256 _ibtRate) {
    if (block.timestamp >= expiry) {
        if (!ratesAtExpiryStored) {
            storeRatesAtExpiry();
        }
    }

    // Decrease ptRate based on detected negative yield 
    (_ptRate, _ibtRate) = _getPTandIBTRates(false);
    if (block.timestamp < expiry) {
        if (_ibtRate != ibtRate) {
            ibtRate = _ibtRate;
        }
        if (_ptRate != ptRate) {
            ptRate = _ptRate;
        }
    }
    // No protection against repeated calls
}
```

### Summary

Lack of protection against repeated `_updatePTandIBTRates` calls allows attackers to arbitrarily drain PT rate. This could lead to loss of collateral value from the protocol.

## Tools Used
Vs

## Recommended Mitigation Steps
Add a `ptRateUpdateCount` state variable and limit `_updatePTandIBTRates` to 1 call per transaction.


## Assessed type

Reentrancy