xanoutas/solidity-security-findings huggingface dataset:
Owner can be attacked by a malicious contract when the owner calls withdraw, thereby draining the users assets.
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

## Report Details:

In the `withdraw` function, there is a potential vulnerability related to reentrancy. The `IERC4626(ibt).withdraw` call is executed before an essential state is set, potentially exposing the contract to reentrancy attacks. To mitigate this risk and enhance security, consider introducing a reentrancy guide in the `withdraw` function.

Here is the existing code snippet for reference:

```solidity
function withdraw(
    uint256 assets,
    address receiver,
    address owner
) public override returns (uint256 shares) {
    _beforeWithdraw(assets, owner);
    (uint256 _ptRate, uint256 _ibtRate) = _getPTandIBTRates(false);
    uint256 ibts = IERC4626(ibt).withdraw(assets, receiver, address(this));
    shares = _withdrawShares(ibts, receiver, owner, _ptRate, _ibtRate);
}
```

## Vulnerability Summary:

The current contract design exhibits a vulnerability where a malicious receiver contract can exploit the withdrawal mechanism, allowing an attacker to repeatedly call the withdrawal function, draining the asset balance and shares of the legitimate owner.

## Exploitation Scenario:

1. **Initiation of Withdrawal:**
   - The legitimate owner initiates a withdrawal, sending a specified amount of assets to a receiver contract.

2. **Trust in Receiver Contract:**
   - The contract places significant trust in the receiver contract, assuming that it will follow the intended behavior.

3. **Reentrancy Exploitation:**
   - The malicious receiver contract can exploit this trust by re-calling the withdrawal function within its own execution context.

4. **Bypassing Safeguards:**
   - The `_beforeWithdraw` hook, responsible for pre-withdrawal checks, validates the owner, checks the time, updates yield, and ensures sufficient assets. However, these checks do not prevent the reentrant exploitation.

5. **Rate Retrieval:**
   - The contract fetches the PT and IBT rates (`_getPTandIBTRates`) as part of the withdrawal process. Unfortunately, this step does not mitigate the attack.

6. **Repeat Withdrawals:**
   - The attacker can repeatedly call the withdrawal function until the asset balance is depleted, leading to an unintended transfer of all shares and assets.

## Impact:

The exploitation of this vulnerability results in a scenario where the legitimate owner's shares and assets are drained by the malicious receiver, causing substantial financial loss.


Explanation:

1. **Vulnerability Concern:**
   - The `IERC4626(ibt).withdraw` call is made before executing important state changes within the `_withdrawShares` function.

2. **Reentrancy Risk:**
   - Executing external calls before setting crucial states opens the possibility of reentrancy attacks, where an external contract could manipulate the contract's state during the execution of the `withdraw` function.
Wrong decimals utilized in computingYield if underlying asset has less decimals than Yield Token
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
In the [_computeYield()](https://github.com/code-423n4/2024-02-spectra/blob/main/src/libraries/PrincipalTokenUtil.sol#L55-L130) function if there was more negative yield that happened the function will try and calculate the yield in the following way:
```solidity
else {
                    // either both negative and positive yield happened, more negative
                    // or only negative yield happened
                    uint256 actualNegativeYieldInAssetRay = _convertToAssetsWithRate(
                        userYTBalanceInRay,
                        _oldPTRate - _ptRate,
                        RayMath.RAY_UNIT,
                        Math.Rounding.Floor
                    );
                    uint256 expectedNegativeYieldInAssetRay = Math.ceilDiv(
                        ibtOfPTInRay * (_oldIBTRate - _ibtRate),
                        RayMath.RAY_UNIT
                    );
                    yieldInAssetRay = expectedNegativeYieldInAssetRay >
                        actualNegativeYieldInAssetRay
                        ? 0
                        : actualNegativeYieldInAssetRay - expectedNegativeYieldInAssetRay;
                    yieldInAssetRay = yieldInAssetRay.fromRay(
                        /// INFO: What if the underlying asset is USDC with 6 decimals, so it will divide by 1e21
                        IERC4626(IPrincipalToken(IYieldToken(_yt).getPT()).underlying()).decimals()
                    ) < SAFETY_BOUND /// INFO: this is set to 100, it is a constant that is hardcoded
                        ? 0
                        : yieldInAssetRay;
                }
```
The problem arises in the following conversion 
```solidity
                    yieldInAssetRay = yieldInAssetRay.fromRay(
                    IERC4626(IPrincipalToken(IYieldToken(_yt).getPT()).underlying()).decimals()
                    ) < SAFETY_BOUND 
                        ? 0
                        : yieldInAssetRay;
```
In the beginning of the [_computeYield()](https://github.com/code-423n4/2024-02-spectra/blob/main/src/libraries/PrincipalTokenUtil.sol#L55-L130) function ``userYTBalanceInRay`` and ``uint256 ibtOfPTInRay = userYTBalanceInRay.mulDiv(_oldPTRate, _oldIBTRate);`` are calculated in the following way
```solidity
        uint256 userYTBalanceInRay = IYieldToken(_yt).actualBalanceOf(_user).toRay(
            IYieldToken(_yt).decimals()
        );
        // ibtOfPT is the yield generated by each PT corresponding to the YTs that the user holds
        uint256 ibtOfPTInRay = userYTBalanceInRay.mulDiv(_oldPTRate, _oldIBTRate);
```
If for example the underlying token for the vault is USDC which has 6 decimals on most chains, the YieldToken when deployed has 18 decimals by default. And the underlying ERC4626 Vault which represents the IBT for the ``PrincipalToken.sol`` contract also has 18 decima
Incorrect calculation of `newYieldInIBTRay` in function `_computeYield`.
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The calculation of `newYieldInIBTRay` in function `_computeYield` is incorrect, causing less new yield.

## Proof of Concept
As shown in [_computeYield#L71-L76](https://github.com/code-423n4/2024-02-spectra/blob/main/src/libraries/PrincipalTokenUtil.sol#L71-L76), the calculation of `newYieldInIBTRay` can be described as `newYield = oldYield * (newIBTRate - oldIBTRate) / newIBTRate`, where `libOfPTInRay` is `oldYield` and `_ibtRate` is `newIBTRate`. The yield increases proportionally to rate, so the denominator should be `oldIBTRate` instead of `newIBTRate`.

```solidity
71:        // ibtOfPT is the yield generated by each PT corresponding to the YTs that the user holds
72:        uint256 ibtOfPTInRay = userYTBalanceInRay.mulDiv(_oldPTRate, _oldIBTRate);
73:        if (_oldPTRate == _ptRate && _ibtRate > _oldIBTRate) {
74:            // only positive yield happened
75:->          newYieldInIBTRay = ibtOfPTInRay.mulDiv(_ibtRate - _oldIBTRate, _ibtRate);
76:        } else {
```
https://github.com/code-423n4/2024-02-spectra/blob/main/src/libraries/PrincipalTokenUtil.sol#L71-L76

## Tools Used
Manual Review

## Recommended Mitigation Steps
Fix the calculation of new yield as follows.
```solidity
-            newYieldInIBTRay = ibtOfPTInRay.mulDiv(_ibtRate - _oldIBTRate, _ibtRate);
+            newYieldInIBTRay = ibtOfPTInRay.mulDiv(_ibtRate - _oldIBTRate, _oldIBTRate);
```


## Assessed type

Math
`Principal::deposit` @ line 171 invokes the function without slippage protection
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The external function [`Principal::deposit @ line 171`](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L171) invokes a function with the same name [`deposit() @ line 176`](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L176) which doesn't have the slippage protection. See code blocks below.

`deposit() @ line 171`
```solidity
function deposit(uint256 assets, address receiver) external override returns (uint256 shares) {
    shares = deposit(assets, receiver, receiver); // @audit invokes desposit() @ line 176
}
```
`deposit() @ line 176`
```solidity
function deposit(
    uint256 assets,
    address ptReceiver,
    address ytReceiver
) public override returns (uint256 shares) {
    IERC20(_asset).safeTransferFrom(msg.sender, address(this), assets);
    IERC20(_asset).safeIncreaseAllowance(ibt, assets);
    uint256 ibts = IERC4626(ibt).deposit(assets, address(this));
    shares = _depositIBT(ibts, ptReceiver, ytReceiver);
}
```
If the user calls `deposit() @ line 171`, he/she is susceptible to inconceivable shares loss because of the lack of slippage protection. However, there's another function with the same name that also invokes `deposit() @ line 176` with a slippage protection. It's the deposit function `deposit() @ line 188`, shown below.
```solidity
function deposit(
    uint256 assets,
    address ptReceiver,
    address ytReceiver,
    uint256 minShares
) external override returns (uint256 shares) {
    shares = deposit(assets, ptReceiver, ytReceiver);
    if (shares < minShares) {   // @audit has slippage protection
        revert ERC5143SlippageProtectionFailed();
    }
}
```
This is the function that should have been invoked. 

## Tools Used
Manual Review

## Recommended Mitigation Steps
Invoke the `deposit() @ line 188` within `deposit() @ line 171` by changing the code as shown below.

```diff
-- function deposit(uint256 assets, address receiver) external override returns (uint256 shares) {
++ function deposit(uint256 assets, address receiver, uint256 minShares) external override returns (uint256 shares) {

--     shares = deposit(assets, receiver, receiver);
++     shares = deposit(assets, receiver, receiver, minShares);
}
```


## Assessed type

Other
In line 303, `PrincipalToken::withdrawIBT` allows users to withdraw without slippage protection
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Since the function [PrincipalToken::withdrawIBT @ line 303](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L303) is exposed `public`, the users can call / withdraw. Unfortunately, that function has no slippage protection as shown below.
```solidity
function withdrawIBT(
    uint256 ibts,
    address receiver,
    address owner
) public override returns (uint256 shares) { // @audit this is `public` but should just be `internal`
    _beforeWithdraw(IERC4626(ibt).previewRedeem(ibts), owner);
    (uint256 _ptRate, uint256 _ibtRate) = _getPTandIBTRates(false);
    shares = _withdrawShares(ibts, receiver, owner, _ptRate, _ibtRate);
    // send IBTs from this contract to receiver
    IERC20(ibt).safeTransfer(receiver, ibts);
}
```
It was invoked on another function with the same name [PrincipalToken::withdrawIBT @ line 316](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L316) but this time, it has slippage protection as shown below.

```solidity
function withdrawIBT(
    uint256 ibts,
    address receiver,
    address owner,
    uint256 maxShares
) external override returns (uint256 shares) {
    shares = withdrawIBT(ibts, receiver, owner); // @audit invoked the function `withdrawIBT` line 303
    if (shares > maxShares) {
        revert ERC5143SlippageProtectionFailed();
    }
}
```
If the user withdraws via function `withdrawIBT() @ line 303`, he/she is susceptible to inconceivable `ibts` loss. The user must withdraw via `withdrawIBT() @ line 316` because it has the "slippage protection" and it should be explicitly and intentionally enforced.

## Tools Used
Manual Review


## Recommended Mitigation Steps
The quickest fix is to change `withdrawIBT @ line 303` from `public` to `internal`as shown below. 

```diff
function withdrawIBT(
    uint256 ibts,
    address receiver,
    address owner
-- ) public override returns (uint256 shares) {
++ ) internal override returns (uint256 shares) {
    _beforeWithdraw(IERC4626(ibt).previewRedeem(ibts), owner);
    (uint256 _ptRate, uint256 _ibtRate) = _getPTandIBTRates(false);
    shares = _withdrawShares(ibts, receiver, owner, _ptRate, _ibtRate);
    // send IBTs from this contract to receiver
    IERC20(ibt).safeTransfer(receiver, ibts);
}
```
This will prevent the users from withdrawing ibt via this function. The users then will be forced to use the "proper" withdraw function which is `withdrawIBT() @ line 316` which has a slippage protection.


## Assessed type

Other
In line 278, `PrincipalToken::withdraw` allows users to withdraw without slippage protection
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
In [PrincipalToken::withdraw @ line 278](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L278) since the function is exposed `public`, the users can call / withdraw. Unfortunately, that function has no slippage protection as shown below.
```solidity
function withdraw(
    uint256 assets,
    address receiver,
    address owner
) public override returns (uint256 shares) {
    _beforeWithdraw(assets, owner);
    (uint256 _ptRate, uint256 _ibtRate) = _getPTandIBTRates(false);
    uint256 ibts = IERC4626(ibt).withdraw(assets, receiver, address(this));
    shares = _withdrawShares(ibts, receiver, owner, _ptRate, _ibtRate);
}
```
It was invoked on another function with the same name [PrincipalToken::withdraw @ line 290](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L290-L300) but this time, it has slippage protection as shown below. 

```solidity
function withdraw(
    uint256 assets,
    address receiver,
    address owner,
    uint256 maxShares
) external override returns (uint256 shares) {
    shares = withdraw(assets, receiver, owner);
    if (shares > maxShares) {
        revert ERC5143SlippageProtectionFailed();
    }
}
```
If the user withdraws via function `withdraw() @ line 278`, he/she is susceptible to inconceivable `asset` loss. The user must withdraw via `withdraw() @ line 290` because it has the "slippage protection" and it should be explicitly and intentionally enforced.

## Tools Used
Manual Review

## Recommended Mitigation Steps
The quickest fix is to change `withdraw() @ line 278` from `public` to `internal`as shown below. 

```diff
function withdraw(
    uint256 assets,
    address receiver,
    address owner
-- ) public override returns (uint256 shares) {
++ ) internal override returns (uint256 shares) {
    _beforeWithdraw(assets, owner);
    (uint256 _ptRate, uint256 _ibtRate) = _getPTandIBTRates(false);
    uint256 ibts = IERC4626(ibt).withdraw(assets, receiver, address(this));
    shares = _withdrawShares(ibts, receiver, owner, _ptRate, _ibtRate);
}
```
This will prevent the users from withdrawing via this function. The users then will be forced to use the "proper" `withdraw` function which is `withdraw() @ line 290` which has a slippage protection.


## Assessed type

Other
In line 253, `PrincipalToken::redeemForIBT` allows users to redeem without slippage protection
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
In [PrincipalToken::redeemForIBT @ line 253](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L253-L262) users are allowed to redeem `ibts` without slippage protection because the function is exposed `public` as shown below.

```solidity
function redeemForIBT(
    uint256 shares,
    address receiver,
    address owner
) public override returns (uint256 ibts) { // @audit set as "public", should just be "internal"
    _beforeRedeem(shares, owner);
    ibts = _convertSharesToIBTs(shares, false);
    IERC20(ibt).safeTransfer(receiver, ibts);
    emit Redeem(owner, receiver, shares);
}
```
It was invoked on another function with the same name [PrincipalToken::redeemForIBT @ line 265](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L265-L275) but this time, it has slippage protection as shown below.
```
function redeemForIBT(
    uint256 shares,
    address receiver,
    address owner,
    uint256 minIbts
) external override returns (uint256 ibts) {
    ibts = redeemForIBT(shares, receiver, owner); // @audit redeemForIBT @ line 253 was invoked here
    if (ibts < minIbts) {
        revert ERC5143SlippageProtectionFailed();
    }
}
``` 
If the user calls `PrincipalToken::redeemForIBT @ line 253`, he/she is susceptible to inconceivable `ibts` loss. To redeem for ibts, the user must call `PrincipalToken::redeemForIBT @ line 265` because it has the "slippage protection" and it should be explicitly and intentionally enforced. 

## Tools Used
Manual Review

## Recommended Mitigation Steps
The quickest fix is to make `PrincipalToken::redeemForIBT @ line 253` from `public` to `internal` as shown below. 

```diff
function redeemForIBT(
    uint256 shares,
    address receiver,
    address owner
-- ) public override returns (uint256 ibts) {
++ ) internal override returns (uint256 ibts) {
    _beforeRedeem(shares, owner);
    ibts = _convertSharesToIBTs(shares, false);
    IERC20(ibt).safeTransfer(receiver, ibts);
    emit Redeem(owner, receiver, shares);
}

```
This will prevent the users from calling this function and instead use the "proper" redeemForIBT function which is `PrincipalToken::redeemForIBT @ line 265` which has the slippage protection.


## Assessed type

Other
A misbehaving IBT can cause the protocol to lose all accumulated fees.
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details


## Impact

NOTE: Negative effects from a "bad" IBT should be out of scope since it is the responsibility of the users to check the quality of the IBT. However, **I checked this with Yanis from the protocol team**, if a "bad" IBT would cause the protocol to lose fees, then it is a valid issue. 

The protocol obtains tokenization, yield and flashloan fees from users utilising the protocol and stores it in PrincipalToken IBT account. If an IBT were to misbehave by having a extreme ibtRate (either by error in design or malicious intent), YT owners would be able to claim enourmous yields and empty the contract balance. 
Since the fees are stored in the same account from which yield is paid, the above scenario would also cause the protocol to lose all the ibt they accumulated as a result of their fees. 

The `claimFees()` function redeems the ibt to the underlying asset, but this does not resolve the issue since users can choose if they wish to claim their yield in ibt or underlying asset. And since those claimed fees remain on the PT balance, they are affected when the contract balance gets drained. 

## Proof of Concept

Assume we have an IBT with ibtrate of 5. 

Due to an error in design or malicious attack, the ibtRate jumps to 5_000_000. 

Any YT owner would be able to call `claimYield` and claim an enormous amount. If sufficient owners call the function, the entire balance would go to zero and PT owners would be left pennyless. 

But since Spectra is permissionless, it the responsibility of the investors to check the quality of the IBT. 

However, since the fees are also stored in the same account balance from yield is paid, the very last tranfers of yield will consist of these accumulated fees. As a result, the protocol loses all the fees they accumulated over time. 

## Tools Used

Manual Review

## Recommended Mitigation Steps

Automitically transfer any fee to a treasury address the moment it is added.  



## Assessed type

ERC4626
In line 229, `PrincipalToken::redeem` allows users to redeem assets without slippage protection
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
In [PrincipalToken::redeem @ line 229](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L229-L237) users are allowed to redeem `assets` without slippage protection because the function is exposed `public` as shown below.

```solidity
function redeem(
    uint256 shares,
    address receiver,
    address owner
) public override returns (uint256 assets) { // @audit set as "public", should just be "internal"
    _beforeRedeem(shares, owner);
    assets = IERC4626(ibt).redeem(_convertSharesToIBTs(shares, false), receiver, address(this));
    emit Redeem(owner, receiver, shares);
}
```
It was invoked on another function with the same name [`PrincipalToken::redeem @ line 240`](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L240-L250) but this time, it has slippage protection as shown below. 
```solidity
function redeem(
    uint256 shares,
    address receiver,
    address owner,
    uint256 minAssets
) external override returns (uint256 assets) {
    assets = redeem(shares, receiver, owner); // @audit redeem() @ line 229 was invoked here
    if (assets < minAssets) {
        revert ERC5143SlippageProtectionFailed();
    }
}
```
If the user calls the function `PrincipalToken::redeem @ line 229`, he/she is susceptible to inconceivable `assets` loss. The user must call `PrincipalToken::redeem @ line 240` because it has the "slippage protection" and it should be explicitly and intentionally enforced.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Change the function `PrincipalToken::redeem @ line 229` from `public` to `internal` as shown below.

```diff
function redeem(
    uint256 shares,
    address receiver,
    address owner
-- ) public override returns (uint256 assets) {
++ ) internal override returns (uint256 assets) {
    _beforeRedeem(shares, owner);
    assets = IERC4626(ibt).redeem(_convertSharesToIBTs(shares, false), receiver, address(this));
    emit Redeem(owner, receiver, shares);
}
```
This will prevent the users from calling this function and forces them to use `PrincipalToken::redeem @ line 240` which has the slippage protection.


## Assessed type

Other