xanoutas/solidity-security-findings huggingface dataset:
Incorrect rounding in `_convertIBTsToShares()`/`_convertSharesToIBTs()`, leading to minting more shares for the user than intended
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details


## Impact

The rounding of `_convertIBTsToShares()` and `_convertSharesToIBTs()` function is incorrect, and does not always benefit the protocol. This could result in the minting of more shares for the user than intended, disadvantaging the protocol.

## Bug Description

There are two rounding issues in the code:

1. `_convertIBTsToShares()`, the more important issue, which may lead to user minting more PT/YTs than intended.
2. `_convertSharesToIBTs()`, the less important issue, does not cause any main issues at the moment, but is still coded incorrectly.

### 1. `_convertIBTsToShares()`

The `_convertIBTsToShares` function is designed to convert IBTs into PT/YT shares, the `_roundUp` parameter dictates the rounding direction. In scenarios where `_roundUp == false`, indicating a preference for rounding down. The current implementation incorrectly passes false to the `_getPTandIBTRates` function. This results in rounding down of the `_ptRate`, which, upon division to calculate the share amount, leads to rounding up the final share quantity.

This means even if the function is called with `_roundUp == false`, it may still round up. This discrepancy can impact the `_depositIBT` function, leading it to mint an amount of shares for the user that is slightly more than what is strictly proportional, favoring the user over the protocol. This could allow users to gain additional value, which is not the intended behavior.

```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
        );
    }
    ...
    function _getCurrentPTandIBTRates(bool roundUpPTRate) internal view returns (uint256, uint256) {
        uint256 currentIBTRate = IERC4626(ibt).previewRedeem(ibtUnit).toRay(_assetDecimals);
        if (IERC4626(ibt).totalAssets() == 0) { // @auditnote: Q: Why check totalSupply != 0?
            currentIBTRate = 0;
        }
        uint256 currentPTRate = currentIBTRate < ibtRate
            ? ptRate.mulDiv(
                currentIBTRate,
                ibtRate,
                roundUpPTRate ? Math.Rounding.Ceil : Math.Rounding.Floor
            )
            : ptRate;
        return (currentPTRate, currentIBTRate);
    }
    function _getPTandIBTRates(bool roundUpPTRate) internal view returns (uint256, uint256) {
        if (ratesAtExpiryStored) {
            return (ptRate, ibtRate);
        } else {
            return _getCurrentPTandIBTRates(roundUpPTRate);
Invariant Violation: Discrepancy in PT and YT Token Supplies
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Summary
In the `_withdrawShares` and `_beforeRedeem` functions of the `PrincipalToken` contract, the burning process fails to properly account for both PT and YT tokens, leading to unequal total supplies.

## Vulnerability Details
The code fails to maintain the invariant where the Principal Token (PT) and its associated Yield Token (YT) should always have equal supplies. Despite attempts to adjust the YT balance to 0 after expiration, incorrect burning functions prevent equalization.

invariant : https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/README.md?plain=1#L142
```solidity
 //function _withdrawShares

       // burn owner's shares (YT and PT)
        if (block.timestamp < expiry) {
            IYieldToken(yt).burnWithoutUpdate(_owner, shares);
        }
        _burn(_owner, shares);
...
```
```solidity
//function _beforeRedeem

        } else {
            updateYield(_owner);
            IYieldToken(yt).burnWithoutUpdate(_owner, _shares);
        }
        _burn(_owner, _shares);
...
```
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L793-L796
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L816-L820

## Impact
Violation of the invariant results in discrepancies in PT and YT supplies.
Inaccurate supply figures may cause confusion and inconsistency in token-related operations.
Users and integrators relying on accurate supply information may face challenges.

## Proof of Concept
1. **withdraw**
**Deposit**
- The user deposits 100 Usdc into the contract by calling the `PrincipalToken::deposit` function.
- The `PrincipalToken::_depositIBT` function mint 100 PT and 100 YT tokens to the user's address.
- Finally, Total supply of PT and YT increses by 100
**withdraw**
- User calls `PrincipalToken::withdraw` function after maturity
- withdraw function will call `PrincipalToken::_withdrawShares` 
- This line will not be called `IYieldToken(yt).burnWithoutUpdate(_owner, shares);` and YT total supply remains unchanged but PT total supply will be decrease 


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

        uint256 beforeBalance = underlying.balanceOf(testUser);
        assertEq(yt.totalSupply(), principalToken.totalSupply());

        _increaseTimeToExpiry();

        principalToken.withdraw(amount, testUser, testUser);

        assertEq(yt.totalSupply(), principalToken.totalSupply());

    }
```
2. **Redeem*
_getCurrentPTandIBTRates return err value of currentPTRate
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
`currentIBTRate < ibtRate` should be `currentIBTRate > ibtRate`
if current IBT Rate is bigger than old IBT rate, then need calculate the new ptRate

## Recommended Mitigation Steps
    uint256 currentPTRate = currentIBTRate > ibtRate
            ? ptRate.mulDiv(
                currentIBTRate,
                ibtRate,
                roundUpPTRate ? Math.Rounding.Ceil : Math.Rounding.Floor
            )
            : ptRate;





## Assessed type

Error
Duration Paramter in Principle token can be exploited to cause IBT Rate Maniplulation and Gas Griefing 
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The Spectra protocol allows users to deposit an Interest-Bearing Token (IBT) which is split into Principle Token (PT) and Yield Token (YT) pairs in a 1:1 ratio. However, the protocol lacks proper validation for the duration parameter provided during initialization. An attacker can exploit this by setting the duration parameter to zero or a very short duration, causing continuous deposit and withdrawal cycles. This can lead to instability in interest baring token rates, as the protocol mentioned clearly that "IBT rate is only updated upon user interactions with our protocol" and it also disruption of PT and YT supply equilibrium,  and enable gas-griefing or denial-of-service attacks.
 
## Proof of Concept
The vulnerable code segment can be found in the initialize function of the Spectra protocol. By setting the duration parameter to zero or a very short duration, an attacker can trigger rapid deposit and withdrawal cycles.
``solidity 
    function initialize(
        address _ibt,
        uint256 _duration,
        address _initialAuthority
    ) external initializer {
        if (_ibt == address(0) || _initialAuthority == address(0)) {
            revert AddressError();
        }
        if (IERC4626(_ibt).totalAssets() == 0) {
            revert RateError();
        }
        _asset = IERC4626(_ibt).asset();
        duration = _duration;
        expiry = _duration + block.timestamp;
        string memory _ibtSymbol = IERC4626(_ibt).symbol();
``
## Attack scenario 
The Attacker deploys the `principle token` and initializes the contracts by setting the duration to zero or very short period, now the attacker begins a series of rapid deposit and withdrawal cycles. They deposit IBT tokens into the protocol, receive PT and YT pairs, and immediately withdraw them. By doing so attacker maniputes IBT rates and also causing disruption of Pt and Yt supply.
The continuous deposit and withdrawal cycles initiated by the attacker result in significant gas costs for legitimate users. This gas-griefing behavior can disrupt normal protocol operations and lead to denial-of-service attacks against the Spectra protocol.
## Tools Used
Manual review
## Recommended Mitigation Steps
Ensure that the duration parameter is set to a reasonable value, preventing immediate expiration.
Implement rate-limiting mechanisms or cooldown periods to prevent rapid deposit and withdrawal cycles


## Assessed type

DoS
In line 206, `PrincipalToken::depositIBT` allows users to deposit 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#L206-L213


# Vulnerability details

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

```solidity
function depositIBT(
    uint256 ibts,
    address ptReceiver,
    address ytReceiver
) public override returns (uint256 shares) { // @audit set as "public", should just be "internal"
    IERC20(ibt).safeTransferFrom(msg.sender, address(this), ibts);
    shares = _depositIBT(ibts, ptReceiver, ytReceiver);
}
```
It was invoked on another function with the same name [`PrincipalToken::depositIBT @ line 216`](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L216-L226) but this time, it has slippage protection as shown below. 

```solidity
function depositIBT(
    uint256 ibts,
    address ptReceiver,
    address ytReceiver,
    uint256 minShares
) external override returns (uint256 shares) {
    shares = depositIBT(ibts, ptReceiver, ytReceiver); // @audit depositIBT @ line 206 was invoked here
    if (shares < minShares) {
        revert ERC5143SlippageProtectionFailed();
    }
}
```
If the user calls `depositIBT() @ line 206`, he/she is susceptible to inconceivable shares loss. The entry point of the user (to deposit ibts) must be `depositIBT() @ line 216` because it has the "slippage protection" and it should be explicitly and intentionally enforced.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Change `PrincipalToken::depositIBT @ line 206` from `public` to `internal` as shown below.

```diff
function depositIBT(
    uint256 ibts,
    address ptReceiver,
    address ytReceiver
-- ) public override returns (uint256 shares) {
++ ) internal override returns (uint256 shares) {
    IERC20(ibt).safeTransferFrom(msg.sender, address(this), ibts);
    shares = _depositIBT(ibts, ptReceiver, ytReceiver);
}
```
This will prevent the users from using this function as an entry point to deposit `ibts`. The users then will be forced to use the "proper" deposit function which is `depositIBT() @ line 216` which has a slippage protection.


## Assessed type

Other
`PrincipalToken::claimYield()` will allow users to claim yield before `expiry/maturity`.
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

**Description:**
In practical scenarios, claiming earnings should only be possible after `expiry/maturity` when calling `claimYield()`. However, in the current situation, users can call `claimYield()` before `expiry/maturity`.

**Impact:**
1. Users can prematurely call `claimYield()` to receive earnings at an inappropriate time or under conditions more favorable to themselves.
2. Furthermore, based on the [documentation description](https://dev.spectra.finance/guides/tokenizing-yield#id-5.-after-expiry): "After expiry/maturity, the token holder could: Claim the remaining yield of the YT see claimYield()". It is clear that users should not be able to claim earnings before `expiry/maturity`.

**Additional Note: Impact - 02**
The issue with `PrincipalToken::claimYieldInIBT()` is similar to the issue with `PrincipalToken::claimYield()`, as both can be called before `expiry/maturity`.

**Recommended Mitigation:**
It is recommended that developers add an `notExpired` modifier to ensure that `claimYield()` and `claimYieldInIBT()` can only be called to receive earnings after `expiry/maturity`.








## Assessed type

Access Control
Exchange rate can be manipulated with Flashloans
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

During flashloans, the vault will lend ibts(asset) without changing the number of shares issued which could lead to the exchange rate fluctuating as flash loans are served.

Read more here: https://www.euler.finance/blog/exchange-rate-manipulation-in-erc4626-vaults

## Proof of Concept

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

## Tools Used

Manual Review

## Recommended Mitigation Steps

To solve this problem, track the amount of the ibts tokens(asset) that have been flashlent then add to the total assets held by the vault


## Assessed type

Other
No check for Flashlender sending back the tokens
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

There is no check to see if the flashlender actually transfers back the funds which could lead to loss of funds if there no return of tokens

## Proof of Concept

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

## Tools Used

Manual Review

## Recommended Mitigation Steps

Use a require statement like this; 

require(IERC20(ibt).safeTransferFrom(address(_receiver), address(this), _amount + fee, "Flashlender: Repay failed");


## Assessed type

Other
In line 176, `PrincipalToken::deposit` allows users to deposit 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#L176


# Vulnerability details

## Impact
In [PrincipalToken::deposit @ line 176](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L176) users are allowed to deposit `assets` without slippage protection because the function is exposed `public` as shown below.
```solidity
function deposit(
    uint256 assets,
    address ptReceiver,
    address ytReceiver
) public override returns (uint256 shares) {// @audit set as "public", should just be "internal"
    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);
}
```
It was invoked on another function with the same name [PrincipalToken::deposit @ line 188](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L188) but this time, it has slippage protection as shown below. 

```solidity
function deposit(
    uint256 assets,
    address ptReceiver,
    address ytReceiver,
    uint256 minShares
) external override returns (uint256 shares) {
    shares = deposit(assets, ptReceiver, ytReceiver); // @audit deposit @ line 176 was invoked here
    if (shares < minShares) {
        revert ERC5143SlippageProtectionFailed();
    }
}
```
If the user calls `deposit() @ line 176`, he/she is susceptible to inconceivable shares loss. The entry point of the user (to deposit assets) must be `deposit() @ line 188` 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 `deposit() @ line 176` internal as shown below. 

```diff
function deposit(
    uint256 assets,
    address ptReceiver,
    address ytReceiver
--  ) public override returns (uint256 shares) {
++  ) internal 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);
}
```
This will prevent the users from using this function as an entry point to deposit. The users then will be forced to use the "proper" deposit function which is `deposit() @ line 188` which has a slippage protection.


## Assessed type

Other
The `PrincipalToken::updateYield` function lacks access control, which could result in arbitrary users being able to update their IBT yield.
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L340-L366
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L369-L374
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L848-L859
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L879-L894


# Vulnerability details

**Description:** The `PrincipalToken::updateYield` function lacks any access control, potentially allowing an attacker to arbitrarily update the IBT yield of any user who has made deposits.

**Impact - 01:**
1. An attacker could use the `updateYield()` function to update the IBT yield of any user who has made deposits.
2. Additionally, the [documentation](https://dev.spectra.finance/technical-reference/contract-functions/principal-token#updateyield) states that "This method should not be used by users." Therefore, I consider this a serious lack of access control that poses a risk.


**Additional Note: Impact - 02**
Malicious users can exploit the issue in the `updateYield()` function to maximize their profits when calling `claimYield()` to receive earnings. The analysis is as follows:
1. Because the business logic of calling `claimYield()` is as follows: `claimYield() -->> _claimYield() -->> updateYield() -->> _updatePTandIBTRates()`
2. Therefore, malicious users can preemptively call `updateYield()`, use `_updatePTandIBTRates()` to update `_ptRate` and `_ibtRate` to their advantage. This allows them to maximize profits when calling `claimYield` and earn more income.

**Recommended Mitigation:**
It is recommended to add access control so that only authorized personnel can call this function. Alternatively, consider changing the visibility of the function to `internal` instead of `public`.





## Assessed type

Access Control
Malicious Users Can Drain The Assets Of Vault. (Due to not being ERC4626 Complaint)
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
Malicious users can drain the `assets(ibts)` of the vault.

## Proof of Concept
The withdraw function users convertToShares to convert the assets to the amount of shares. These shares are burned from the users account and the assets(ibt) are returned to the user.
Here is the `withdraw` function 
```
    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);
    }
```
Here it is converted to the assets by burning the adequate shares, Here if _ibtRate > _ptRate then it rounds down which is not suggested by OZ.
```
  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(//@note here rounding down happens
            _ptRate,
            _ibtRate,
            _roundUp ? Math.Rounding.Ceil : Math.Rounding.Floor
        );
    }
```

## Recommended Mitigation Steps
Use `Math.Rounding.Up` instead of `Math.Rounding.Down`.

As per OZ implementation here is the rounding method that should be used:

`deposit` : `convertToShares` → `Math.Rounding.Down`

`mint` : `converttoAssets` → `Math.Rounding.Up`

`withdraw` : `convertToShares` → `Math.Rounding.Up`

`redeem` : `convertToAssets` → `Math.Rounding.Down`



## Assessed type

Math