xanoutas/solidity-security-findings huggingface dataset:
Volatile IBT rate will quickly degrade PT rate of the pricipal token
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L906-L913


# Vulnerability details

## Impact
Vault which IBT rate often alternates between increasing and decreasing will quickly push the principal token PT rate to a low number.

## Proof of Concept
Let's look at how PT and IBT rates are updated in the `PrincipalToken.sol`
```solidity
        uint256 currentPTRate = currentIBTRate < ibtRate
            ? ptRate.mulDiv(
                currentIBTRate,
                ibtRate,
                roundUpPTRate ? Math.Rounding.Ceil : Math.Rounding.Floor
            )
            : ptRate;
        return (currentPTRate, currentIBTRate);
    }
```
If the previous IBT rate is greater than the current one, we calculate the new PT rate as `PTnew = PTold * IBTnew / IBTold`, the PT rate never increases, and can only decrease.

Let's say we have a vault which IBT rate = 1 has decreased to 0.9, as a result the PT rate will be 1 * 0.9 / 1 = 0.9, after some time the IBT rate increased to 1 and then decreased to 0.9 again, thus decreasing PT rate even more 0.9 * 0.9 / 1 = 0.81. It's easy to notice that if these small fluctuations continue, the PT rate will soon go down to a small number.

Coded POC for `PrincipalToken.t.sol`

```solidity
    function testPTRateDegrade() public {
        uint256 amountToDeposit = 100e18;
        underlying.approve(address(principalToken), amountToDeposit);

        // initial deposit
        principalToken.deposit(amountToDeposit, address(this));

        for(uint256 i=0; i<20; i++){
            // IBT rate goes down by 5%
            _increaseRate(-5);
            // poke update rate
            principalToken.updateYield(address(this));
            // IBT rate goes up by 5%
            _increaseRate(5);
            principalToken.updateYield(address(this));
        }
        ibtRate = principalToken.getIBTRate();
        ptRate = principalToken.getPTRate();
        console.log("IBT PT: ", ibtRate, ptRate);
    }
``` 

It will take twenty 5 % fluctuations to decrease the PT rate to 0.34

## Tools Used
Foundry
## Recommended Mitigation Steps
Perhaps we need to decrease the PT rate only if the IBT rate updated it's minimum
```solidity
    function _getCurrentPTandIBTRates(bool roundUpPTRate) internal view returns (uint256, uint256) {
        uint256 currentIBTRate = IERC4626(ibt).previewRedeem(ibtUnit).toRay(_assetDecimals);
        if (IERC4626(ibt).totalAssets() == 0 && IERC4626(ibt).totalSupply() != 0) {
            currentIBTRate = 0;
        }
        uint256 currentPTRate;
>>      if (currentIBTRate < ibtRateMin) {
            currentPTRate = ptRate.mulDiv(
                currentIBTRate,
                ibtRate,
                roundUpPTRate ? Math.Rounding.Ceil : Math.Rounding.Floor
            )
>>          ibtRateMin = ibtRate;
        } else {
            currentPTRate = ptRate;
        }
        return (currentPTRate, currentIBTRate);
    }
```


## Assessed type

ER
Incompatible with rebasing tokens.
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L868


# Vulnerability details

## Impact
There are some tokens such as stETH which is a rebasing token. Rebasing tokens can change the balance of the user to maintain the desired peg for the token.
Rebasing tokens aren't supported because when a user deposits, the user balance in the protocol for the IBT will be fixed. This will lead to the user losing funds when the token rebases.

## Proof of Concept

Consider the following scenario:

 - Alice deposits her IBT.
 - After a while she wants to withdraw her IBT.
 - But the token rebases to increase the token balance of its users and so when Alice withdraws she'll receive the original amount deposited and not increased amount caused by the rebase.


## Recommended Mitigation Steps
Consider blacklisting rebasing tokens.

## Reference
https://github.com/code-423n4/2023-10-wildcat-findings/issues/503


## Assessed type

ERC20
Improper Implementation of Slippage Protection Mechanism
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Users could end up with fewer shares than they anticipated based on the assets they provided due to the incorrect implementation of slippage protection. Instead of ensuring a minimum number of shares received, the current implementation limits the maximum shares which does not align with the typical use case of slippage protection

## Proof of Concept
Both `withdraw` and `withdrawIBT` functions in the `PrincipalToken` contract utilize a `maxShares` parameter intended for slippage protection. However, the naming and implementation logic of this parameter suggests a focus on limiting the maximum number of shares to be received on given assets. 
```
 if (shares > maxShares) {
            revert ERC5143SlippageProtectionFailed();
        }
```

In financial transactions, the primary concern typically revolves around ensuring users do not receive too few shares rather than too many which is not the case here.

The slippage protection should guard users against unfavorable exchange rates that result in receiving an insufficient number of shares for their assets or IBTs.

## Tools Used
Manual Review
## Recommended Mitigation Steps
Rename the `maxShares` parameter to `minShares` to accurately reflect its purpose in slippage protection and adjust the conditional checks to ensure the number of calculated shares is not less than `minShares`


## Assessed type

Other
Broken Invariant: Equal Supply PT==YT
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/YieldToken.sol#L57-L61


# Vulnerability details

## Impact

The protocol stated the following invariant in the contest README.md: 
- "PT and its YT should have an equal supply at all times"


However, any owner of YT tokens can simply call `YieldToken.burn` to burn a part or all of the YT he owns. 

In which case the invariant is broken since then the supply of PT > supply of YT.

## Tools Used

Manual Review

## Recommended Mitigation Steps
Add the same access control as is used in `burnWithoutUpdate`



## Assessed type

Access Control
Rounding error when deposit will result in more shares being minted to users
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L885
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L684


# Vulnerability details

When the user calls `deposit`, the function internally uses `_convertIBTsToShares` to calculate how many shares should be minted after charging fees.
```solidity
    function _convertIBTsToShares(
        uint256 _ibts,
        bool _roundUp
    ) internal view returns (uint256 shares) {
1>      (uint256 _ptRate, uint256 _ibtRate) = _getPTandIBTRates(false);
        if (_ptRate == 0) {
            revert RateError();
        }
        shares = _ibts.mulDiv(
            _ibtRate,
            _ptRate,
            _roundUp ? Math.Rounding.Ceil : Math.Rounding.Floor
        );
    }
```
The `_convertIBTsToShares` function gets the current `ptRate` and `ibtRate` by calling `_getPTandIBTRates`. `ptRate` and `ibtRate` are updated in the previous `updateYield` function (Because this is calculated in the same transaction).


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

When deposit is called, `updateYield` will call `_updatePTandIBTRates` to update `ptRate` and `ibtRate`. Since the parameter of the `_getPTandIBTRates` function in `_updatePTandIBTRates` is also false, it will obviously cause `ptRate` to be rounded down.
This means that due to rounding down, the ptRate is smaller than expected, ultimately causing more shares to be mint.

In the `_convertIBTsToShares` function, the protocol attempts to round down shares via `Math.Rounding.Floor` when calculating:
```solidity
        shares = _ibts.mulDiv(
            _ibtRate,
            _ptRate,
            _roundUp ? Math.Rounding.Ceil : Math.Rounding.Floor
        );
```
But since `_ptRate` is also rounded down, it may mint more shares.

## Impact

When depositing, if `ibtRate` decreases, it will cause `ptRate` to be rounded down, thus mint more shares to users. This is contrary to what the sponsor said `Decimals imprecisions should always benefit the protocol`.

## Proof of Concept

Consider the following scenario:
1. IBT continues to be at negative rate
2. Alice deposits her assets
2.1 updateYield() is called, causing ptRate to be rounded down
2.2 _convertIBTsToShares() is called, and although this function attempts to round down shares via `Math.Rounding.Floor` when calculating, a low `ptRate` may still cause the shares to be mint more than expected.
3. The protocol mints more shares than expected
Incorrect implementation of the EIP-5095 standard for maxRedeem() and maxWithdraw()
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L482-L485
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L458-L462


# Vulnerability details

## Impact

The EIP-5095 standard states that the function MUST factor in both global and user-specific limits, like if redemption is entirely disabled (even temporarily) it MUST return 0.

When the protocol is paused and redemption is temporarily disabled, this function should return 0. 


Yet, both functions returns the value as if redemption is still enabled, which is a clear-cut violation of the EIP-5095 standard. 



## Proof of Concept


From: [EIP-5095](https://eips.ethereum.org/EIPS/eip-5095)
```
maxRedeem  

Maximum amount of principal tokens that can be redeemed from the holder balance, through a redeem call.

MUST return the maximum amount of principal tokens that could be transferred from holder through redeem and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary).

MUST factor in both global and user-specific limits, like if redemption is entirely disabled (even temporarily) it MUST return 0.

MUST NOT revert.
```

```
maxWithdraw

Maximum amount of the underlying asset that can be redeemed from the holder principal token balance, through a withdraw call.

MUST return the maximum amount of underlying tokens that could be redeemed from holder through withdraw and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary).

MUST factor in both global and user-specific limits, like if withdrawals are entirely disabled (even temporarily) it MUST return 0.

MUST NOT revert.
```

`PrincipalToken.sol`
```
    /** @dev See {IPrincipalToken-maxWithdraw}.
     */
    function maxWithdraw(address owner) public view override whenNotPaused returns (uint256) {
        return convertToUnderlying(_maxBurnable(owner));
    }
```


`PrincipalToken.sol`
```
    /** @dev See {IPrincipalToken-convertToUnderlying}. */
    function convertToUnderlying(uint256 principalAmount) public view override returns (uint256) {
        return IERC4626(ibt).previewRedeem(_convertSharesToIBTs(principalAmount, false));
    }
```

`PrincipalToken.sol`
```

    /** @dev See {IPrincipalToken-maxRedeem}. */
    function maxRedeem(address owner) public view override returns (uint256) {
        return _maxBurnable(owner);
    }
```

`PrincipalToken.sol`
```
    /**
     * @notice Computes the maximum amount of burnable shares for a user
     * @param _user The address of the user
     * @return maxBurnable The maximum amount of burnable shares
     */
    function _maxBurnable(address _user) internal view returns (uint256 maxBurnable) {
        if (block.timestamp >= expiry) {
            maxBurnable = balanceOf(_user);
Incorrect implementation of the EIP-5095 standard for covertToUnderlying()
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L492-L495
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L680-L693


# Vulnerability details

## Impact

The EIP-5095 standard states that the amount returned MUST NOT show any variations depending on the caller.

Yet, the `convertToUnderlying` function utilises `previewDeposit`.

When we check the `previewDeposit` EIP-4626 standard, it states that it MUST be inclusive of deposit fees. Since deposit fees can vary depending on the caller and the specific ERC4626 implementation (f.e. fee reduction as is used in the spectra protocol), it cannot be guaranteed that there is no variation in the amount returned for every caller. 


The EIP-5095 standard also states that before maturity, the amount of underlying returned is as if the PTs would be at maturity.

However, the `convertToUnderlying` function utilises `_convertIBTsToShares`, which uses current rates. These rates are subject to change and have no relation to the rates at maturity. 


## Proof of Concept


From: [EIP-5095](https://eips.ethereum.org/EIPS/eip-5095)

```
convertToUnderlying

The amount of underlying that would be exchanged for the amount of PTs provided, in an ideal scenario where all the conditions are met.

Before maturity, the amount of underlying returned is as if the PTs would be at maturity.

MUST NOT be inclusive of any fees that are charged against redemptions.

MUST NOT show any variations depending on the caller.

MUST NOT reflect slippage or other on-chain conditions, when performing the actual redemption.

MUST NOT revert unless due to integer overflow caused by an unreasonably large input.

MUST round down towards 0.

This calculation MAY NOT reflect the “per-user” price-per-principal-token, and instead should reflect the “average-user’s” price-per-principal-token, meaning what the average user should expect to see when exchanging to and from.
```

`PrincipalToken.sol`
```

    /** @dev See {IPrincipalToken-convertToUnderlying}. */
    function convertToUnderlying(uint256 principalAmount) public view override returns (uint256) {
        return IERC4626(ibt).previewRedeem(_convertSharesToIBTs(principalAmount, false));
    }
```

`IERC4626.sol`
```
    /**
     * @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given
     * current on-chain conditions.
     *
     * - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit
     *   call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called
     *   in the same transaction.
     * - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the
     *   deposit would be accepted, regardless if the user has enough
PrincipalToken is not ERC-5095 compliant
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L483-L485
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L460-L462
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L278-L287
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L229-L237


# Vulnerability details

## Impact
Protocols that try to integrate with Spectra, expecting `PrincipalToken` to be ERC-5095 compliant, will face an array of issues that may damage Spectra's brand and limit Spectra's growth in the market.

## Proof of Concept

All official ERC-5095 requirements are on their [official page](https://eips.ethereum.org/EIPS/eip-5095). Non-compliant methods are listed below along with why they are not compliant and code POCs demonstrating the issues. To run the POCs, copy-paste them into `PrincipalToken.t.sol`:

### `PrincipalToken::redeem` and `PrincipalToken::withdraw`

As specified in ERC-5095, both `withdraw` and `redeem` must support a flow where `msg.sender` has approval over the owner's tokens.

```
MUST support a redeem flow where the Principal Tokens are burned from holder directly where holder is msg.sender or msg.sender has EIP-20 approval over the principal tokens of holder.
```

```
MUST support a withdraw flow where the principal tokens are burned from holder directly where holder is msg.sender or msg.sender has EIP-20 approval over the principal tokens of holder.
```

However, neither `PrincipalToken::redeem` nor `PrincipalToken::withdraw` support this flow type.

```solidity
    //copy-paste into `PrincipalToken.sol`
    function testRedeemDoesNotSupportERC20ApprovalFlow() public {
        uint256 amountToDeposit = 1e18;
        uint256 expected = _testDeposit(amountToDeposit, address(this));
        _increaseTimeToExpiry();
        principalToken.storeRatesAtExpiry();

        principalToken.approve(MOCK_ADDR_5, UINT256_MAX);
        assertEq(principalToken.allowance(address(this), MOCK_ADDR_5), UINT256_MAX);

        vm.startPrank(MOCK_ADDR_5);
        vm.expectRevert();
        //Should not revert as MOCK_ADDR_5 has allowance over tokens.
        principalToken.redeem(expected, MOCK_ADDR_5, address(this));
        vm.stopPrank();
    }
    
    function testWithdrawDoesNotSupportERC20ApprovalFlow() public {
        uint256 amount = 1e18;
        underlying.approve(address(principalToken), amount);
        principalToken.deposit(amount, testUser);

        principalToken.approve(MOCK_ADDR_5, UINT256_MAX);
        assertEq(principalToken.allowance(address(this), MOCK_ADDR_5), UINT256_MAX);

        vm.prank(MOCK_ADDR_5);
        vm.expectRevert();
        //Should not revert as MOCK_ADDR_5 has allowance over tokens.
        principalToken.withdraw(amount, MOCK_ADDR_5, testUser);

        vm.stopPrank();
    }
```


### `PrincipalToken::maxWithdraw`

According to ERC-5095, `maxWithdraw` must not revert and mu
The ERC-5095 standard is not followed correctly
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L483-L484
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L441-L443


# Vulnerability details

## Impact

Functionality is not working as expected 

## Proof of Concept

For the `maxRedeem` function, in [EIP-5095](https://eips.ethereum.org/EIPS/eip-5095) it is stated :

> MUST factor in both global and user-specific limits, like if redemption is entirely disabled (even temporarily) it MUST return 0
> 

Spectra has a pause function which can deactivate the `Deposit`, `Redeem` and `Withdraw` functions. The main problem is that when `Redeem` is paused the return value of `maxRedeem` should be `0` but in this case it is not, the return value of `maxRedeem` will still return what it usually does even when the `Redeem` function is disabled. 

This happens because when the user call the `maxRedeem` function it will produce a return value from the `_maxBurnable` function. The code can be seen below :

```solidity
    function maxRedeem(address owner) public view override returns (uint256) {
        return _maxBurnable(owner);

		
    function _maxBurnable(address _user) internal view returns (uint256 maxBurnable) {
        if (block.timestamp >= expiry) {
            maxBurnable = balanceOf(_user);
        } else {
            uint256 ptBalance = balanceOf(_user);
            uint256 ytBalance = IYieldToken(yt).balanceOf(_user);
            maxBurnable = (ptBalance > ytBalance) ? ytBalance : ptBalance;
        }
    }
```

As can be seen in the codebase, this function does not have a modifier or logic that differentiates when the main Redeem function is in pause / unpause state so whatever state the return value is in, it will still return what it usually does. This also applies to the [maxDeposit](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L441) function, that function also does not follow existing standards and has the same problem. 

Because `PrincipalToken` must follow EIP-5095 is main invariant, this issue breaks the main invariant and there are not just one but two functions that do not follow the existing standard. It can be concluded :

**Impact:** Medium, as functionality is not working as expected but without a value loss

**Likelihood:** Medium, as multiple methods are not compliant with the standard

## Tools Used

Manual review

## Recommended Mitigation Steps

Go through the [standards](https://eips.ethereum.org/EIPS/eip-5095) and follow it all.


## Assessed type

Other