xanoutas/solidity-security-findings huggingface dataset:
Unclaimed yield of the user will be lost.
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Bug description
Yield in the protocol is determined by the `ibtRate`. An increase in `ibtRate` results in a positive yield for users.
[updateYield()](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L340-L366)
```solidity
function updateYield(address _user) public override returns (uint256 updatedUserYieldInIBT) {
        (uint256 _ptRate, uint256 _ibtRate) = _updatePTandIBTRates();

        uint256 _oldIBTRateUser = ibtRateOfUser[_user];
        if (_oldIBTRateUser != _ibtRate) {
            ibtRateOfUser[_user] = _ibtRate;
        }
        uint256 _oldPTRateUser = ptRateOfUser[_user];
        if (_oldPTRateUser != _ptRate) {
            ptRateOfUser[_user] = _ptRate;
        }

        // Check for skipping yield update when the user deposits for the first time or rates decreased to 0.
        if (_oldIBTRateUser != 0) {
            updatedUserYieldInIBT = PrincipalTokenUtil._computeYield(
                _user,
                yieldOfUserInIBT[_user],
                _oldIBTRateUser,
                _ibtRate,
                _oldPTRateUser,
                _ptRate,
                yt
            );
            yieldOfUserInIBT[_user] = updatedUserYieldInIBT;
            emit YieldUpdated(_user, updatedUserYieldInIBT);
        }
    }
```
The above function is responsible for updating the yield for a user. It does so by calling `_computeYield()` of the `PrincipalTokenUtil` library.
Assume a scenario where there are two users, each holding 100 PT/YT tokens after a deposit of 100 tokens of underlying. Let's say `ibtRate` increases by 50%, now userA claims his yield, but userB does not. After that `ibtRate` decreases by 33% to its original value and both users claim their yield.
This will result in userA receiving 49 underlying tokens of yield, while userB will receive only 33 underlying tokens, which results in 16 tokens of yield lost for userB. While userA got rewarded for a positive increase in `ibtRate`, userB did not.
> **Important** Actual yield of userA is 16 tokens and the actual yield of userB is 0, since 33 tokens are the compensation for the negative decrease of 33%.
## Impact
Loss of yield for users.
## Proof of Concept
Please add this POC to `PrincipalToken3.t.sol` and run it with `forge test --match-test 'testLostYield' -vv`.
```solidity
function testLostYield() public {
        // Current IBTRate = 1e27
        // Current PTRate = 1e27
        address user1 = makeAddr("user1");
        vm.prank(user1);
        underlying.approve(address(principalToken), type(uint256).max);
        address user2 = makeAddr("user2");
        vm.prank(user2);
        underlying.approve(address(principalToken), type(uint256).max);
        // Mint both users 100 USDC initially
        underlying.mint(user1
Using `_mint()` can be dangerous
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Using `PrincipalToken::_mint()` and `YieldToken::_mint` can mint tokens to addresses which don't support these tokens. Use `_safeMint()` instead of `_mint()`. Calling mint this way does not ensure that the receiver is able to accept them.

## Vulnerability Details
 Found in src/tokens/PrincipalToken.sol [Line: 766](src/tokens/PrincipalToken.sol#L766)

	```solidity
	        _mint(_ptReceiver, shares);
	```

- Found in src/tokens/YieldToken.sol [Line: 54](src/tokens/YieldToken.sol#L54)

	```solidity
	        _mint(to, amount);  
	```
## Proof of Concept
 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L248
## Tools Used
manual review

## Recommended Mitigation Steps
Use _safeMint() instead of mint()


## Assessed type

Other
`ibtRate` will be wrong when ERC4626 decimals are different from underlying
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/perspectivefi/spectra-core/blob/fec59dc6720fb4861b07b30845ef2c1a42f947bf/src/tokens/PrincipalToken.sol#L151


# Vulnerability details

## Impact
EIP-4626 does not require the decimals must be the same as the underlying tokens' decimals, and when it's not, previewRedeem may return an inflated or deflated `ibtRate`.

## Proof of Concept
In the current implementation, IERC4626(ibt).decimals() is used as the IERC4626(token).asset()'s decimals to calculate the `ibtRate` in the following formula.

`_ibtDecimals = IERC4626(_ibt).decimals();`

`ibtUnit = 10 ** _ibtDecimals;`

`ibtRate = IERC4626(ibt).previewRedeem(ibtUnit).toRay(_assetDecimals);`

The value of `ibtRate` will be significantly underestimated when the underlying token's decimals > ERC4626's decimals, and be significantly overestimated when the underlying token's decimals < ERC4626's decimals.

This will then cause issues when using the `ibtRate` to calculate how many shares passed to as an argument to withdraw from the ERC4626 as seen in the code below.

```
    function _withdrawShares(
        uint256 _ibts,
        address _receiver,
        address _owner,
        uint256 _ptRate,
        uint256 _ibtRate
    ) internal returns (uint256 shares) {
        if (_ptRate == 0) {
            revert RateError();
        }
        // convert ibts to shares using provided rates
        shares = _ibts.mulDiv(_ibtRate, _ptRate, Math.Rounding.Ceil);
        // burn owner's shares (YT and PT)
        if (block.timestamp < expiry) {
            IYieldToken(yt).burnWithoutUpdate(_owner, shares);
        }
        _burn(_owner, shares);
        emit Redeem(_owner, _receiver, shares);
    }
```

## Tools Used

Manul Review.


## Recommended Mitigation Steps

Set the `ibtUnit` to the decimals of the underlying token `assetDecimals` instead of the ibt token.


## Assessed type

Oracle
`_computeTokenizationFee()` ends by collecting excess fee
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/main/src/libraries/PrincipalTokenUtil.sol#L157-L170


# Vulnerability details

## Impact
Due to Double Round Up, `_computeTokenizationFee()` in `PrincipalTokenUtil.sol` result in taking more fees.

## Proof of Concept

```solidity
    function _computeTokenizationFee(
        uint256 _amount,
        address _pt,
        address _registry
    ) internal view returns (uint256) {
        return
            _amount
                .mulDiv(IRegistry(_registry).getTokenizationFee(), FEE_DIVISOR, Math.Rounding.Ceil) 
                .mulDiv(
                    FEE_DIVISOR - IRegistry(_registry).getFeeReduction(_pt, msg.sender),
                    FEE_DIVISOR,
                    Math.Rounding.Ceil
                );
    }
```

As during calculation of fee, there is 2 times Round_Up happening, so resulted fee will 1 excess.
There should be only one Round_up occurs during fee calculation.

## Tools Used
Manual Review

## Recommended Mitigation Steps
```diff
-       return
-            _amount
-               .mulDiv(IRegistry(_registry).getTokenizationFee(), FEE_DIVISOR, Math.Rounding.Ceil) // @audit-issue excess roundup
-               .mulDiv(
                    FEE_DIVISOR - IRegistry(_registry).getFeeReduction(_pt, msg.sender),
                    FEE_DIVISOR,
                    Math.Rounding.Ceil
                );

+       uint256 nume = _amount * IRegistry(_registry).getTokenizationFee();
+       uint256 deno = FEE_DIVISOR * FEE_DIVISOR;
+       return nume.mulDiv(
                    FEE_DIVISOR - IRegistry(_registry).getFeeReduction(_pt, msg.sender),
                    deno,
                    Math.Rounding.Ceil
                );
    }
```


## Assessed type

Math
Both `_convertIBTsToShares()` and `_convertIBTsToSharesPreview()` use differnt logics to calculate `shares` and `_convertIBTsToSharesPreview()` is incompatable with developer comment
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Share calculation will be different in case of `_convertIBTsToShares()` and `_convertIBTsToSharesPreview()` function.

## Description
Here both `_convertIBTsToShares()` and `_convertIBTsToSharesPreview()` used to calculate resulting amount of shares.

_convertIBTsToShares() called by _depositIBT() as follows
```solidity
shares = _convertIBTsToShares(_ibts - tokenizationFee, false);
```

In `_convertIBTsToShares()` approch is
- PT calculation is Rounding_down
- share calculation is Rounding_down

In `_convertIBTsToSharesPreview` approch is 
- PT calculation is Rounding_up
- share calculation is Rounding_down

so `_convertIBTsToShares()` and `_convertIBTsToSharesPreview` result in different value of shares

```solidity
    function _convertIBTsToShares(
        uint256 _ibts,
        bool _roundUp
    ) internal view returns (uint256 shares) {
        (uint256 _ptRate, uint256 _ibtRate) = _getPTandIBTRates(false);
        if (_ptRate == 0) {
            revert RateError();
        }
        shares = _ibts.mulDiv(
            _ibtRate,
            _ptRate,
            _roundUp ? Math.Rounding.Ceil : Math.Rounding.Floor
        );
    }
```
```solidity

    /**
     * @dev Converts amount of IBT to amount of PT shares with current rates.
     * This method also rounds the result of the new PTRate computation in case of negative rate
     * @param ibts amount of IBT to convert to shares
     * @return shares resulting amount of shares
     */
    function _convertIBTsToSharesPreview(uint256 ibts) internal view returns (uint256 shares) { // @audit-issue _convertIBTsToSharesPreview and _convertIBTsToShares use diferent logics
        (uint256 _ptRate, uint256 _ibtRate) = _getPTandIBTRates(true); // to round up the shares, the PT rate must round down
        if (_ptRate == 0) {
            revert RateError();
        }
        shares = ibts.mulDiv(_ibtRate, _ptRate);
    }
```
## Tools Used
Manual Review

## Recommended Mitigation Steps
Try to implement same logic in 2 function.


## Assessed type

Context
PrincipalToken not compliant with EIP-5095
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L806-L808
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L829-L831
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L460


# Vulnerability details

## Impact

PrincipalToken not compliant with EIP-5095. This can render unusable external integrations.

## Proof of Concept

1. [withdraw](https://eips.ethereum.org/EIPS/eip-5095#withdraw) 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. But contract [allows](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L806-L808) only owner to withdraw 
2. Same for [redeem](https://eips.ethereum.org/EIPS/eip-5095#redeem) ([code](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L806-L808))
3. [maxRedeem](https://eips.ethereum.org/EIPS/eip-5095#maxredeem) MUST not revert, but it [reverts](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L460) in case of protocol pause

## Tools Used

Manual review

## Recommended Mitigation Steps

1. Remove owner check in `_beforeRedeem/Withdraw` and add following lines (adapted from openzeppelin ERC4626):
```
if (msg.sender != owner) {
    _spendAllowance(owner, caller, shares);
}
```
2. Remove `whenNotPaused` modifier from `maxWithdraw` 


## Assessed type

Other
PT and its YT will not have an equal supply at all times
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

The Spectra protocol listed this invariant as one of the main invariants of the protocol:

> PT and its YT should have an equal supply at all times

However, the YieldToken contract's burn function allows users to violate this invariant easily.

## Proof of Concept

The burn function in the YieldToken contract is public for anyone to call:

```solidity
    function burn(uint256 amount) public override {
        IPrincipalToken(pt).updateYield(msg.sender);
        _burn(msg.sender, amount);
    }
```

This function will update the user yield, then burn the given amount directly. Thus, when user burn any amount of YieldToken, the code will goes against its specification and breaks an invariant of the protocol.

This function first updates the user's yield and then directly burns the specified amount of tokens. Consequently, any token burn by a user will go against the code specification and breaks an invariant of the protocol.

## Tools Used

Manual review

## Recommended Mitigation Steps

Consider preventing users from burning tokens.


## Assessed type

Invalid Validation
Users can burn YT when `PrincipalToken` contract is paused by 
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

When the `PrincipalToken` contract has been paused (`_paused` set to true), users should NOT be able to burn YT tokens. This can be inferred as the `_beforeWithdraw()` function has the `whenNotPaused` and it is called before every single withdraw (or the next call in the stack). In other words withdraws are paused. Also during withdraw the yield is updated and YT tokens are burned (only in `_beforeRedeem`). 

```javasript
    function _beforeRedeem(uint256 _shares, address _owner) internal nonReentrant whenNotPaused {
        if (_owner != msg.sender) {
            revert UnauthorizedCaller();
        }
        if (_shares > _maxBurnable(_owner)) {
            revert UnsufficientBalance();
        }
        if (block.timestamp >= expiry) {
            if (!ratesAtExpiryStored) {
                storeRatesAtExpiry();
            }
        } else {
            updateYield(_owner);
            IYieldToken(yt).burnWithoutUpdate(_owner, _shares);
        }
        _burn(_owner, _shares);
    }
```

There is also another way for a user to update his yield and burn tokens which is through `YieldToken::burn()`
```javascript
    function burn(uint256 amount) public override {
        IPrincipalToken(pt).updateYield(msg.sender);
        _burn(msg.sender, amount);
    }
```
However this function does not check if the protocol is paused.

## Impact
Users are able to burn tokens when the system is paused, which they should not be able to do. This could be a problem if the protocol ever needs to temporarily pause for upgrade or the functionality has a bug.

## Proof of Concept
Add the following test to `PrincipalToken.t.sol` and run with `forge test --match-test testBurn -vvv`

```javascript
    function testBurnWhenPaused() public {
        uint256 amountToDeposit = 100e18;
        _testDeposit(amountToDeposit, address(this));

        uint256 amountToBurn = yt.actualBalanceOf(address(this)) / 2; // burn half
        uint256 ytBalanceBefore = yt.actualBalanceOf(address(this));

        vm.expectEmit(false, false, false, true); // verify paused
        emit Paused(scriptAdmin);
        vm.prank(scriptAdmin);
        principalToken.pause();

        yt.burn(amountToBurn);
        uint256 ytBalanceAfter = yt.actualBalanceOf(address(this));

        assertEq(
            ytBalanceBefore,
            ytBalanceAfter + amountToBurn
        );
    }
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
Add `whenNotPaused` modifier to `PrincipalToken::updateYield`. 
Optional: Remove it from `_beforeRedeem` and `_beforeWithdraw` as both methods call `updateYield`.


## Assessed type

Other
The post-expiration price rate can be frontran/sandwiched, permanently setting manipulated price 
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The first call to `storeRatesAtExpiry` after expiration sets the rate of `ptRate, ibtRate` permanently. However, the vault price at this point can be manipulated e.g. with a sandwich attack, by providing a lot of `underlying` liquidity right before the update, and withdrawing it right after, however, the rate in the Spectra protocol e.g. for `ibt` will remain like this forever after the expiration period. This will freeze the price at unreal level, allowing attacker to either get discount for future redeems, or in a reverse case slightly increasing the rate, causing other users to get unfavourable redeems onwards. 

## Proof of Concept
Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Following sequence of attack is possible:
- an attacker who is a malicious node operator or a MEV searcher, finds the tx with call to `storeRatesAtExpiry()`. 
- the attacker might use a flashloan, or self owned large portion of underlying assets, to supply it to the underlying vault and call the `storeRatesAtExpiry()` immediately after, then the result `currentIBTRate` will be abnormally higher. The attacker can redeem back
- at this point, the `ibtRate` returned by `_getCurrentPTandIBTRates` in fragment:

```
uint256 currentIBTRate = IERC4626(ibt).previewRedeem(ibtUnit).toRay(_assetDecimals);
```

will return less value (due to vault having much fewer assets inside) than normally. `currentIBTRate` wil be then permanently lowered

- this will cause `_getPTandIBTRates` to return stored lower `ibtRate` from now on e.g. during a `withdraw` in following fragment:

```
shares = _withdrawShares(ibts, receiver, owner, _ptRate, _ibtRate);

```
and in [_withdrawShares](https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L780)
```
shares = _ibts.mulDiv(_ibtRate, _ptRate, Math.Rounding.Ceil);
```

which translates to: 

`shares = (_ibts * _ibtRate) / _ptRate`

The formula shows that attacker can profit off permanently increased `_ibtRate`, by increasing the numerator and giving him more shares than he should receive. The final result is, the last user to withdraw/redeem might not be able to do so due to lack of liquidity - the last user(s) to redeem withdraw will be then those who lose funds.


## Tools Used
Manual approach.

## Recommended Mitigation Steps
It might still be good idea to use underlying vault for calculating the price after expiration. Otherwise, use some TWAP-like rates accumulated over time, but ultimately do not rely on one-time price update to set the permanent price.

















## Assessed type

Timing
`DOMAIN_SEPARATOR()`  is missing in `PrincipalToken.sol` and `YieldToken.sol` can lead to Missing `DOMAIN_SEPARATOR()` can lead to vulnerabilities  
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
As protocol mentioned `PrincipalToken` and `YieldToken` support ERC2612: 
The `DOMAIN_SEPARATOR()` function in ERC2612 is an important part of the security of the standard. It is used to prevent replay attacks, which occur when a malicious user records a valid signed message and later sends it again to fraudulently perform an action on behalf of the original signer.

The `DOMAIN_SEPARATOR()` is generated based on specific contract parameters, including the contract's address, the chain ID, and a unique identifier. These parameters ensure that the domain separator is unique to the contract and the chain, and prevent attackers from using the same signature on a different chain or contract.

If the `DOMAIN_SEPARATOR()` function is missing from `PrincipalToken` and ``YieldToken`, it can significantly impact the security of the standard. It can make it easier for attackers to replay valid signatures, since the domain separator provides a crucial part of the uniqueness and security of the signature.

Therefore, it's important to ensure that the `DOMAIN_SEPARATOR()` function is included and properly implemented in any contracts that uses ERC2612.

## Proof of Concept
 This is reference of past audit this will proof it. 
https://solodit.xyz/issues/m-07-domain_separator-is-missing-in-lusdtokensol-code4rena-ethos-reserve-ethos-reserve-contest-git

## Tools Used
Manual Review 

## Recommended Mitigation Steps
To mitigate this risk, it is recommended to follow the ERC2612 specification strictly and ensure that the `DOMAIN_SEPARATOR()` is correctly implemented


## Assessed type

Other