xanoutas/solidity-security-findings huggingface dataset:
`ptRate` can be set to 0, which will freeze funds held by the PrincipalToken contract.
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

## Bug description
PrincipalToken contract allows user to tokenize their yield by depositing underlying assets or shares of the underlying ERC4626 vault.
The rate which is used to issue new principal tokens is determined by the `ibtRate` and `ptRate`. `ibtRate` is the rate at which one `ibtUnit` can be redeemed for the underlying asset, it can increase or decrease and it influences the `ptRate`.
`ibtRate` is updated upon user interaction with the protocol, while `ptRate` is only updated after an accounted negative rate change on the `ibtRate`.

[_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) {
        uint256 currentIBTRate = IERC4626(ibt).previewRedeem(ibtUnit).toRay(_assetDecimals);
        if (IERC4626(ibt).totalAssets() == 0 && IERC4626(ibt).totalSupply() != 0) {
            currentIBTRate = 0;
        }
        uint256 currentPTRate = currentIBTRate < ibtRate
            ? ptRate.mulDiv(
                currentIBTRate,
                ibtRate,
                roundUpPTRate ? Math.Rounding.Ceil : Math.Rounding.Floor
            )
            : ptRate;
        return (currentPTRate, currentIBTRate);
    }
```
The function above shows how these rates are calculated and set. The scenario where the `currentIBTRate` is set to 0 happens in a situation where either IBTs have been hacked and stolen all their deposits or a malicious IBT. I will not review the scenario of malicious IBT as it is out of scope, but I will focus on the first case. 
First of all, let's discuss why and how setting `currentIBTRate` to 0 can lead to problems. As can be seen from the function, when `currentIBTRate` is 0, the calculations of `currentPTRate` will result in:
```
ptRate * 0 / ibtRate = 0
```
This is problematic, because unlike `ibtRate`, `ptRate` can never increase. `ptRate` being 0 leads to the inability for users to ever withdraw their deposited IBTs. This happens due to `redeem()` function calling `_convertSharesToIBTs()`, where calculations of `ibt` to redeem will result in 0, and due to `withdraw()` calling `_withdrawShares()`, which will revert when `ptRate` is 0.

[_convertSharesToIBTs()](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L659-L671)
```solidity
function _convertSharesToIBTs(
        uint256 _shares,
        bool _roundUp
    ) internal view returns (uint256 ibts) {
        (uint256 _ptRate, uint256 _ibtRate) = _getPTandIBTRates(false);
        if (_ibtRate == 0) {
            revert RateError();
        }
        ibts = _shares.mulDiv(
            _ptRate,
            _ibtRate,
The ERC4626 standard is not followed correctly
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L441-L443
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#L483-L485


# Vulnerability details

## Description
As per EIP-4626, the maxDeposit method "MUST factor in both global and user-specific limits, like if deposits are entirely disabled (even temporarily) it MUST return 0.". This is not the case currently, as even if the contract is paused, the maxDeposit method will still return what it usually does.
```solidity
    function maxDeposit(address) external pure override returns (uint256) { 
        return type(uint256).max;
    }
```
`maxMint`
`maxWithdraw`
`maxRedeem`
All of the above functions should return 0 when their respective functions are disabled

## Proof of Concept


## Tools Used
Manual Review

## Recommended Mitigation Steps
All functions listed above should be modified to meet the specifications of EIP-4626.
Go through the EIP-4626 standard and follow it for all methods that override methods from the inherited ERC4626 implementation.


## Assessed type

ERC4626
`PrincipalToken::maxDeposit` will not work for some tokens that don't support `type(uint256).max` amount.
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Loss of functionality.
Tokens that don't support type(uint256).max amount could not be deposited.

## Vulnerability Details

There are tokens that doesn't support `type(uint256).max` amount. So the `maxDeposit` will not work for some tokens like `UNI` or `COMP` who will revert when approve `type(uint256).max` amount.

```javascript
    /** @dev See {IPrincipalToken-maxDeposit}. */
    function maxDeposit(address) external pure override returns (uint256) {
        return type(uint256).max;
    }
```

 

## Tools Used
Manual Review

## Recommended Mitigation Steps
I would suggest approve only the necessary amount of token instead of max amount.


## Assessed type

ERC20
Users will not be able to call `PrincipalToken::withdraw` and `PrincipalToken::redeem` functions when the contract is paused
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
In unusual circumstances when the protocol is paused, users will not be able to withdraw their assets, which they would want to.

## Proof of Concept


Add the following test in `PrincipalToken.t.sol` to observe the proof.

```javascript
    function testWithdrawWhenPaused() public {
        uint256 amount = 1e18;
        underlying.approve(address(principalToken), amount);
        principalToken.deposit(amount, testUser);

        uint256 beforeBalance = underlying.balanceOf(testUser);

        vm.prank(scriptAdmin);
        principalToken.pause();
        principalToken.withdraw(amount, testUser, testUser); // this will revert because the protocol was paused
    }
```

This test will fail with `EnforcedPause` error.

## Tools Used

Manual review

## Recommended Mitigation Steps

```diff
-   function _beforeWithdraw(uint256 _assets, address _owner) internal whenNotPaused nonReentrant {
+   function _beforeWithdraw(uint256 _assets, address _owner) internal nonReentrant {
        ...
    }

-    function _beforeRedeem(uint256 _shares, address _owner) internal nonReentrant whenNotPaused {
+    function _beforeRedeem(uint256 _shares, address _owner) internal nonReentrant {
        ...
    }

-    function maxWithdraw(address owner) public view override whenNotPaused returns (uint256) 
+    function maxWithdraw(address owner) public view override returns (uint256) {
        ...
     }

```






## Assessed type

Other
Concentrating vault's liquidity might enable flashloan-backed vault reset attack leading to partial vault drain
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The `PrincipalToken` allows for easier concentration of a underlying vault's shares in form of the `ibt`. In the same time, it is possible to take a flashloan for all of the `ibt` tokens. If enough vault's shares are concentrated in the PT, then it may enable vault reset attack, explained below, partially draining the underlying vault.

## Proof of Concept

During the lifecycle of an ERC4626 vault, it's shares price related to underlying asset slowly increases. The initial ratio is usually `1 share per 1 unit of asset`, but over time, when vault's supplies grow, the shares become slightly more expensive and for example they cost `1 share for 1.3 or 1.5 unit of an asset`.

Now, considering references: [Twitter thread by Kankodu](https://twitter.com/kankodu/status/1685320745898102784), [OZ issue 3800](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/3800)

The risk of "resetting" the vault exist, if vault shares liquidity is concentrated and someone could redeem most of the shares at once, e.g. by a flashloan, resetting the vault, and depositing assets at discounted price. Then, user can return the shares which are "bought" for less assets, repaying the flashloan and keeping the profit.

Since the protocol aims to gain popularity and each PT is dedicated to one specific vault, it should be considered a real risk, that if a vault becomes popular at a PT, which means, it's shares' liquidity is intensely concentrated in form of ibts, which is incentivized, then the vault may be attacked this way. 

The flow of the attack is following:

- an ERC4626 vault, which gains popularity with Spectra, grows large. At some point the share price is `1.5 per 1 asset unit` 
- the vault has a corresponding PT where large amount of shares is stored as `IBT`
- now, anyone can take a flashLoan for max `IBT` balance. 
- during the flashloan, attacker redeems `IBT` in the underlying vault for assets, almost emptying the vault. 
- As the share price drops close to 1:1, user deposits assets again, gaining more than initially shares (`IBTs`)
- Attacker repays the flashloans, keeping the profit
- As per the reference, the profit is:
 `total flashloan value in asset minus fees * (initial share price - reseted share price)` 

## Tools Used
Manual approach.

## Recommended Mitigation Steps
If flashloans are to be used, consider limiting their amount to only a percentage of IBT balance, instead of full contract balance.




















## Assessed type

ERC4626
PrincipalToken does not comply with ERC5095 standard
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/b35bbf78ad9d0e74e9c8450a0c5c6d35b68f7228/src/tokens/PrincipalToken.sol#L460-L462
https://github.com/code-423n4/2024-02-spectra/blob/b35bbf78ad9d0e74e9c8450a0c5c6d35b68f7228/src/tokens/PrincipalToken.sol#L483-L485


# Vulnerability details

## Impact
`PrincipalToken.sol` does not comply with [ERC5095 standard](https://eips.ethereum.org/EIPS/eip-5095). This may DoS or return unexpected values for users that interact with the contract where users expect the contract to follow ERC5095 rules.

## Proof of Concept
Two functions that do not comply with ERC5095 rules:

#### [`maxWithdraw(address)` function](https://github.com/code-423n4/2024-02-spectra/blob/b35bbf78ad9d0e74e9c8450a0c5c6d35b68f7228/src/tokens/PrincipalToken.sol#L460-L462)

```solidity
function maxWithdraw(address owner) public view override whenNotPaused returns (uint256) {
  return convertToUnderlying(_maxBurnable(owner));
}
```

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

`maxWithdraw` function in `PrincipalToken` contract **reverts** when paused, but it **has to** return 0.

#### [`maxRedeem(address)` function](https://github.com/code-423n4/2024-02-spectra/blob/b35bbf78ad9d0e74e9c8450a0c5c6d35b68f7228/src/tokens/PrincipalToken.sol#L483-L485)

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

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

`maxRedeem` function in `PrincipalToken` contract does not check if the contract is paused, but the function **has to** return 0 if the `PrincipalToken` is paused


## Tools Used
Manual Review

## Recommended Mitigation Steps
Make sure `maxRedeem` and `maxWithdraw` comply with ERC5095 rules.

Below are my suggestions to the `maxWithdraw` and `maxRedeem` functions
`maxWithdraw`:
```solidity
    function maxWithdraw(address owner) public view override returns (uint256) {
        return paused() ? 0 : convertToUnderlying(_maxBurnable(owner));
    }
```

`maxRedeem`:
```solidtiy
    function maxRedeem(address owner) public view override returns (uint256) {
        return paused() ? 0 : _maxBurnable(owner);
    }
```


## Assessed type

Other
The Principal Token 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#L806-L808
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


# Vulnerability details

## Impact

The Principal Token deviate from the ERC-5095 specification which may break external integrations.

## Proof of Concept

ERC-5095 stipulates:

> 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. 

However, the PrincipalToken.redeem function calls the _beforeRedeem function:

```solidity
    function _beforeRedeem(uint256 _shares, address _owner) internal nonReentrant whenNotPaused {
        if (_owner != msg.sender) {
            revert UnauthorizedCaller();
        }
    }    
```

This reverts if the owner is not the sender, thus violating ERC5095.

ERC-5095 also states:

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

As previously explained, the PrincipalToken.redeem calls the _beforeRedeem function, which has a whenNotPaused modifier, meaning redemption can be paused by the owner.

However, the maxRedeem function will return maxBurnable of the user if redemption is paused:

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

It's important to note the same issue exists in the maxWithdraw function, as it will still call convertToUnderlying with the user's maxBurnable even if the contract is paused. maxWithdraw has a whenNotPaused modifier, which will revert, although the ERC clearly states:

> MUST NOT revert.

## Tools Used

Manual review

## Recommended Mitigation Steps

Consider following the ERC-5095 specifications.


## Assessed type

Other
PrincipalToken does not comply with EIP5095
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L229-L237
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L278-L287
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L493-L495
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L488-L490
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L460-L462


# Vulnerability details

## Bug description
Principal Token contract is an ERC5095 vault that allows users to tokenize their yield in a permissionless manner.
> **Important** [The contest page](https://code4rena.com/audits/2024-02-spectra#top) explicitly states that `PrincipalToken` must conform with the [EIP-5095](https://eips.ethereum.org/EIPS/eip-5095).

The following specification of `redeem()` is taken directly from the EIP-5095:
> At or after maturity, burns exactly principalAmount of Principal Tokens from `from` and sends underlyingAmount of underlying tokens to `to`. 
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`.

The following specification of `withdraw()` is taken directly from the EIP-5095:
> Burns `principalAmount` from `holder` and sends exactly `underlyingAmount` of underlying tokens to `receiver`.
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`.

Both these functions should allow an approved user to redeem or withdraw approved Principal tokens, but currently, that's impossible. All `redeem` functions internally call [_beforeRedeem()](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L805-L821), which has this check in place:
```solidity
if (_owner != msg.sender) {
            revert UnauthorizedCaller();
        }
```

The same can be said about every `withdraw` function. All of them call [_beforeWithdraw()](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L828-L842), which has the same check in place.


As per EIP-5095 both `convertToUnderlying()` and `convertToPrincipal()` functions  *MUST NOT revert unless due to integer overflow caused by an unreasonably large input*.
In case of `convertToUnderlying()` function, it calls [_convertSharesToIBTs()](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L659-L672), which can revert with `RateError` when `ibtRate` is 0.
`updateYield()` can be called even when the contract is paused
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-L341


# Vulnerability details

## Impact

Function `updateYield()` misses the `whenNotPaused` modifier - thus it can be called even when the contract is paused.


## Proof of Concept

The `PrincipalToken` inherits from `PausableUpgradeable`, thus it can be paused.
One of the function - `updateYield()`, however, does not implement `whenNotPaused` modifier - meaning, it can be called even when the contract is paused.

[File: PrincipalToken.sol](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L340-L341)
```
    function updateYield(address _user) public override returns (uint256 updatedUserYieldInIBT) {
        (uint256 _ptRate, uint256 _ibtRate) = _updatePTandIBTRates();
```

As demonstrated above, function `updateYield()` does not implement `whenNotPaused` modifier - thus it's possible to call it even when the contract is paused.
Function `updateYield()` calls another functions: `_updatePTandIBTRates()` and `PrincipalTokenUtil._computeYield` - however, none of them verifies if contract is paused - thus it's possible to call `updateYield()` even on the paused contract.

## Tools Used

Manual code review


## Recommended Mitigation Steps

Implement `whenNotPaused` modifier on `updateYield()` function.


## Assessed type

Access Control
Redeem Function May Cause Unintended PT Share Loss
Audit: 2024-02-spectra-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Users attempting to redeem small amounts of shares could lose these shares without receiving any underlying assets or IBTs.

## Proof of Concept
The `redeem` Function allows users to `redeem` their `PT shares` in exchange for the `underlying assets`. It first converts the PT shares to IBTs using `_convertSharesToIBTs` before redeeming them from the IBT contract.
```
        assets = IERC4626(ibt).redeem(_convertSharesToIBTs(shares, false), receiver, address(this));
```

The `_convertSharesToIBTs` Function converts PT shares to IBTs with an option to round up or down the result.
```
  function _convertSharesToIBTs(
        uint256 _shares,
        bool _roundUp
    ) internal view returns (uint256 ibts) {
```
In the redeem function the roundup is set to false meaning it will always round down when converting to IBTs.

When users try to redeem a very small number of PT shares, the conversion might result in an IBT amount of zero due to rounding down consequently the redeemed assets becomes 0. This scenario could lead to a loss of shares for the user without receiving any assets in return. 

Similar case with `redeemIBT` that will result in user being sent 0 IBTs when the shares to be redeemed are too small.

## Tools Used
Manual Review
## Recommended Mitigation Steps
Implement a minimum redeemable share amount to prevent rounding to zero or revert incase of 0 IBTs 


## Assessed type

Math