xanoutas/solidity-security-findings huggingface dataset:
The owner of shares is not verified in the withdraw functions. This could allow anyone to burn someone else's shares via withdraw.
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
It allows anyone to maliciously burn someone else's shares by calling the withdraw functions. Specifically:

The withdraw and withdrawIBT functions allow burning an arbitrary amount of shares belonging to any user by simply specifying their address as the owner parameter.
This could drain all the value from unsuspecting users by improperly redeeming their shares.
Since shares entitle holders to the underlying assets in the vault, being able to improperly burn someone's shares causes them to lose the value they represent.
The severity is high because it violates the basic expectation that only share owners can redeem their own shares. Allowing anyone to burn anyone else's shares completely compromises ownership control. This vulnerability enables theft of funds and should be fixed immediately.

Overall this is a critical severity issue since it leads to direct loss of funds for users. It breaks a core security expectation of non-fungible token ownership in the contract. The impact cannot be overstated - it makes the shares stealable and valueless as valid ownership credentials.

## Proof of Concept
The `withdraw` functions do not verify that the msg.sender is the owner of the shares being withdrawn. This is a vulnerability that could allow anyone to burn someone else's shares.
Here is how it works:
The `withdraw()` and `withdrawIBT()` functions take an owner parameter, which is intended to be the owner of the shares. However, there is no verification that `msg.sender` equals this owner parameter.
For example:

       function withdraw(
           uint256 assets,
           address receiver, 
           address owner
       ) public override returns (uint256 shares) {

           _beforeWithdraw(assets, owner);
    
           // Burn shares based on assets and send assets to receiver
           // No check that msg.sender == owner
       }

This means anyone can call withdraw and pass any arbitrary address as the owner. That address's shares will be burned and assets sent to the receiver.
So the impact is that anyone can burn anyone else's shares, effectively stealing their deposited assets.

1. Alice deposits 100 assets and has 100 shares
2. Eve calls withdraw(50 assets, Eve's address, Alice's address)
3. 50 of Alice's shares are burned and 50 assets sent to Eve
4. Eve has stolen 50 assets by burning Alice's shares


## Tools Used
Manual
## Recommended Mitigation Steps
There should be a check that msg.sender equals the owner in both withdraw functions before burning any shares:

       function withdraw(
           uint256 assets,
           address receiver, 
           address owner
       ) public override returns (uint256 shares) {

            if (msg.sender != owner) {
               revert UnauthorizedCaller(); 
           }

           _beforeWithdraw(asse
Arbitrary `from` passed to `transferFrom` (or `safeTransferFrom`)
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
 Passing an arbitrary `from` address to `transferFrom` (or `safeTransferFrom`) can lead to loss of funds, because anyone can transfer tokens from the `from` address if an approval is made.

## Vulnerability Details
- Found in src/tokens/PrincipalToken.sol [Line: 628](src/tokens/PrincipalToken.sol#L628)

	```solidity
	        IERC20(ibt).safeTransferFrom(address(_receiver), address(this), _amount + fee);
	```

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

	```solidity
	        return super.transferFrom(from, to, amount);
	```

## Proof of Concept
 

## Tools Used
Manual Analysis

 



## Assessed type

Token-Transfer
When storing rates at expiry, there is no check that totalAssets > 0. This could allow rates to be stored when the vault is empty
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
it would set the PT and IBT rates to 0 in the contract state. This would make the PT and IBT tokens worthless and irredeemable, since redeeming them later would attempt to divide by a 0 rate.
This would essentially break the core functionality of withdrawing assets from the vault after expiry. It would likely make the PT and IBT tokens unusable and valueless.
The severity is quite high. This omission essentially invalidates a core intended feature of the contract - allowing withdrawal of assets after expiry based on fixed conversion rates. It could lead to total loss of users' funds if they are unable to withdraw anything after expiry due to the 0 rates.


## Proof of Concept
There is no check that `totalAssets` > 0 when storing rates at expiry. This could potentially allow rates to be stored even if the vault is empty.
Here is the relevant code:

       function storeRatesAtExpiry() public override afterExpiry {
           if (ratesAtExpiryStored) {
               revert RatesAtExpiryAlreadyStored();
            }
          ratesAtExpiryStored = true;
           // PT rate not rounded up here 
           (ptRate, ibtRate) = _getCurrentPTandIBTRates(false); 
           emit RatesStoredAtExpiry(ibtRate, ptRate);
        }

       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; 
           }
            // ...
        }

The `storeRatesAtExpiry()` function calls `_getCurrentPTandIBTRates()` to get the latest rates. This function gets the IBT rate from the vault using `previewRedeem()`, which could return a non-zero rate even if `totalAssets` is 0.
There is no check of totalAssets > 0 before storing the rates.


## Tools Used
Manual
## Recommended Mitigation Steps
Add a check for `totalAssets` > 0 before storing rates:

       function storeRatesAtExpiry() public override afterExpiry {
            if (ratesAtExpiryStored) {
                revert RatesAtExpiryAlreadyStored(); 
           }
           if (IERC4626(ibt).totalAssets() == 0) {
               revert(); // fail if vault empty
            }
   
           ratesAtExpiryStored = true; 
            (ptRate, ibtRate) = _getCurrentPTandIBTRates(false);
            emit RatesStoredAtExpiry(ibtRate, ptRate); 
        }

This prevents empty rates from being stored.



## Assessed type

Other
Relying on `balanceOf` can lead to Price Manupulation Attack
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L499
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L588
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L868
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L870
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L871
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/YieldToken.sol#L121
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/YieldToken.sol#L124
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/YieldToken.sol#L128
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/YieldToken.sol#L129


# Vulnerability details

## Impact
Deriving Price, or rates between assets more generally, can be manipulated from the ratio of balance. Flash loan and donation are the well-known attack vectors used to manipulate the prices.
Example code snippet: https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/YieldToken.sol#L127
```solidity
  /** @dev See {IYieldToken-actualBalanceOf} */
    function actualBalanceOf(address account) public view override returns (uint256) {
        return super.balanceOf(account);
    }
```
 
## Tools Used
Manual review

## Recommended Mitigation Steps
Implement internal accounting instead of relying on `balanceOf`.


## Assessed type

Other
`block.timestamp` does not correspond to the same values on multi chains
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Block time refers to the time separating blocks. The average block time in Ethereum is 12 seconds, but this value is different on different chains on Polygon and in Arbitrum chains.

## Proof of Concept
Consider a Principal Token (PT) with a maturity date set based on block.timestamp.

On Ethereum, this might work predictably, but on Arbitrum and on Polygon chain, the PT could reach maturity earlier or later than expected due to how these other networks handles timestamps. This discrepancy could mislead token holders regarding the maturity of their investments, potentially leading to confusion or loss if tokens appear to mature at unexpected times.

```solidity
// YieldToken.sol
function balanceOf(
        address account
    ) public view override(IYieldToken, ERC20Upgradeable) returns (uint256) {
        return (block.timestamp < IPrincipalToken(pt).maturity()) ? super.balanceOf(account) : 0;
    }

// PrincipalToken.sol
 modifier notExpired() virtual {
        if (block.timestamp >= expiry) {
            revert PTExpired();
        }
        _;
    }
```

## Tools Used

Manual Review 

## Recommended Mitigation Steps

Implement a more flexible maturity calculation and other operations involving `block.timestamp` whilst using the `block.timestamp` values. 



## Assessed type

Invalid Validation
The rewardsProxy code is executed via delegatecall, which is dangerous
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The main risk with using delegatecall for the rewardsProxy is that it gives the rewardsProxy contract full access to the state and logic of the PrincipalToken contract. This could allow a malicious rewardsProxy contract to drain funds, manipulate state like balances or rates, pause the contract, etc.
Some major impacts this could have:
• Drain all deposited assets from the vault by redeeming the IBT and sending funds to the attacker
• Manipulate rates to artificially inflate yields, misleading users
• Freeze deposits/redemptions by pausing the contract
• Transfer ownership/privileges by changing access control
The severity here is very high given the privilege escalation. A malicious rewardsProxy could essentially take full control. Using a simple call instead of delegatecall would limit the blast radius if the rewardsProxy contract itself is compromised. Additional safeguards like rate limiters could help, but avoiding delegatecall is the most robust mitigation.


## Proof of Concept
The use of `delegatecall` to execute the `rewardsProxy` code can be dangerous. Here is a detailed explanation:
The key section of code is:

       function claimRewards(bytes memory _data) external restricted {
           if (rewardsProxy == address(0)) {
               revert NoRewardsProxySet();
           }
           _data = 
       abi.encodeWithSelector(IRewardsProxy(rewardsProxy).claimRewards.selector, 
       _data);
           (bool success, ) = rewardsProxy.delegatecall(_data);
           if (!success) {
               revert ClaimRewardsFailed();
           }
     }
This uses   `delegatecall` to execute the `claimRewards` function on the rewardsProxy contract.
The vulnerability is that `delegatecall` runs the code of the target contract in the context of the calling contract. This means the target contract gains access to the storage, balance etc. of the calling contract.
A malicious rewardsProxy contract could therefore:
• Drain assets from the PrincipalToken contract
• Manipulate storage to steal funds
• Prevent the contract from functioning properly

Proof of Attack:

       // Malicious rewardsProxy contract
       contract Attack {
         function claimRewards(bytes calldata data) external {
           // Drain assets
           IERC20(principalTokenAddress).transfer(attackerAddress, 100000 * 
       10**18);
    
           // Manipulate rates to zero to steal deposits
           IPrincipalToken(principalTokenAddress).ptRate = 0;
           IPrincipalToken(principalTokenAddress).ibtRate = 0;
         } 
       }

       // Call from PrincipalToken invokes delegatecall
       principalToken.claimRewards();

* This is a well-known vulnerability with delegatecall.


## Tools Used
Manual
## Recommended Mitigation Steps
• Don't use delegatecall, make a normal function c
The user's yield is updated in beforeYtTransfer. This means yield will not be updated if only PT is transferred. 
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
User's yield will not be updated if they transfer PT tokens without also transferring YT tokens.
This means that if a user earns yield through their YT tokens, and then transfers PT tokens, their yield will not be incorporated into the Principal Token contract until they also transfer YT tokens.
For users who only interact with PT tokens, this could lead to lost or inaccurate yield tracking. Their earned yield would not be updated or claimable until they also transfer YTs. This violates the expected behavior and is a high severity issue.


## Proof of Concept
The `beforeYtTransfer` function is only called when a YT transfer happens, not when a PT transfer happens. This means that user yield is only updated when YTs are transferred, but not when only PTs are transferred.
Here is the relevant code:

      function beforeYtTransfer(address _from, address _to) external override {
           if (msg.sender != yt) {
               revert UnauthorizedCaller(); 
           }
           updateYield(_from);
           updateYield(_to);
        }
This shows that `updateYield` is only called for the `_from` and `_to` addresses when a YT transfer happens (since only the YT contract can call this function).
The impact is that if a user transfers PTs, their yield will not be updated. This means the yield calculations could be incorrect if the user later redeems or transfers YTs, since the rates would not have been updated.
For example:
1. Alice deposits and has 100 PTs and 100 YTs
2. The yield rate decreases
3. Alice transfers 50 PTs to Bob
4. Alice's yield is not updated since only PTs were transferred
5. Later Alice tries to redeem 50 YTs when the rate is much lower
6. But her yield will be calculated based on the old higher rate, overstating her actual yield
This could allow users to manipulate yield calculations if they transfer only PTs.


## Tools Used
Manual
## Recommended Mitigation Steps
The updateYield function could be called on all PT transfers in the ERC20 _beforeTokenTransfer hook:

       function _beforeTokenTransfer(address _from, address _to, uint256 amount) 
       internal override {
         updateYield(_from); 
         updateYield(_to);
       }
This would ensure yields are updated on any token transfer, preventing manipulation.



## Assessed type

Other
`Principal::depositIBT` @ line 201 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#L201-L202


# Vulnerability details

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

`depositIBT @ line 201`
```solidity
function depositIBT(uint256 ibts, address receiver) external override returns (uint256 shares) {
    shares = depositIBT(ibts, receiver, receiver); // @audit invokes depositIBT() @ line 206
}
```
`depositIBT() @ line 206`
```solidity
function depositIBT(
    uint256 ibts,
    address ptReceiver,
    address ytReceiver
) public override returns (uint256 shares) {
    IERC20(ibt).safeTransferFrom(msg.sender, address(this), ibts);
    shares = _depositIBT(ibts, ptReceiver, ytReceiver);
}
```
If the user calls `depositIBT() @ line 201`, 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 `depositIBT() @ line 206` and this time, with a slippage protection. It's the deposit function [`depositIBT() @ line 216`](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L216-L226), shown below.

`depostIBT() @ line 216`
```solidity
function depositIBT(
    uint256 ibts,
    address ptReceiver,
    address ytReceiver,
    uint256 minShares
) external override returns (uint256 shares) {
    shares = depositIBT(ibts, ptReceiver, ytReceiver); // @audit invokes depostiIBT() @ line 206
    if (shares < minShares) {
        revert ERC5143SlippageProtectionFailed();
    }
}
```
This is the function that should have been invoked. 

## Tools Used
Manual Review

## Recommended Mitigation Steps
Invoke the `depostIBT() @ line 216` within `depositIBT @ line 201` and change the code as shown below.
```diff
-- function depositIBT(uint256 ibts, address receiver) external override returns (uint256 shares) {
++ function depositIBT(uint256 ibts, address receiver, uint256 minShares) external override returns (uint256 shares) {

--    shares = depositIBT(ibts, receiver, receiver);
++    shares = depositIBT(ibts, receiver, receiver, minShares);
}
```





## Assessed type

Other
The convertToPrincipal and convertToUnderlying functions can potentially allow manipulation of rates after expiry.
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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#L493-L495


# Vulnerability details

## Impact
After expiry, users can manipulate rates to get more or less principal tokens for their underlying assets or vice versa when converting between the two. This could benefit certain users unfairly. 
It could enable certain attacks like flash loan arbitrage after expiry by artificially modifying rates. An attacker could take a flash loan, convert it to principal tokens using a manipulated low rate, redeem principal tokens using a manipulated high rate, and profit from it.
It goes against the concept of storing rates at expiry to finalize conversion rates. Allowing manipulation afterwards undermines this. Severity: Medium
Overall, I would consider the severity of this issue as High since it opens up flash loan attack vectors.

## Proof of Concept
The `convertToPrincipal` and `convertToUnderlying` functions can potentially allow manipulation of rates after expiry.
The key issue is that these functions call `previewDeposit` and `previewRedeem` on the IBT even after expiry. Specifically:

       /** @dev See {IPrincipalToken-convertToPrincipal}. */
       function convertToPrincipal(uint256 underlyingAmount) external view 
       override returns (uint256) {
           return 
       _convertIBTsToShares(IERC4626(ibt).previewDeposit(underlyingAmount), 
       false); 
       }

       /** @dev See {IPrincipalToken-convertToUnderlying}. */  
       function convertToUnderlying(uint256 principalAmount) public view override 
       returns (uint256) {
           return 
       IERC4626(ibt).previewRedeem(_convertSharesToIBTs(principalAmount, false));
       }
The `previewDeposit` and `previewRedeem` functions on the IBT use the current IBT rate. However, after expiry the IBT rate could change while the PT rate is fixed based on `storeRatesAtExpiry()`.
This means by manipulating the IBT rate through deposits/redeems after expiry, an attacker could manipulate the `convertToPrincipal` and `convertToUnderlying` view functions to return incorrect results.
For example, an attacker could:
1. Wait until after expiry when rates are stored
2. Deposit assets into the IBT to artificially increase IBT rate
3. Call convertToPrincipal - this will use the new higher IBT rate to convert assets to higher PT shares
4. Withdraw from the IBT to bring rate back down
5. Call convertToUnderlying - this will use the old lower PT rate to convert PT shares to more assets

## Tools Used
Manual
## Recommended Mitigation Steps
the `convertToPrincipal` and `convertToUnderlying` functions should use the stored rates after expiry rather than calling the IBT contract.
For example:

       function convertToPrincipal(uint256 underlyingAmount) external view 
       override retur