xanoutas/solidity-security-findings huggingface dataset:
`withdraw()` does not decrease the allowance
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-L287
https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L828-L841


# Vulnerability details

## Impact

The inverse of the `deposit()` function is a `withdraw()` function. Function `deposit()` deposits amount of assets in the PT vault, while function `withdraw()` burns owner's shares and sends assets to receiver.
While function `deposit()` increases the allowance, similarly - when we withdrawing, function `withdraw()` should decrease that allowance. However, the current implementation of `withdraw()` misses to decrease the allowance.


## Proof of Concept

[File: PrincipalToken.sol](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L176-L185)
```
    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);
    }
```

As demonstrated above - while depositing - function calls `IERC20(_asset).safeIncreaseAllowance(ibt, assets);` to increase the allowance. However, the allowance is not being decreased during the withdrawal.

[File: PrincipalToken.sol](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L278-L287)
```
    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);
    }

```

[File: PrincipalToken.sol](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L828-L841)
```
    function _beforeWithdraw(uint256 _assets, address _owner) internal whenNotPaused nonReentrant {
        if (_owner != msg.sender) {
            revert UnauthorizedCaller();
        }
        if (block.timestamp >= expiry) {
            if (!ratesAtExpiryStored) {
                storeRatesAtExpiry();
            }
        } else {
            updateYield(_owner);
        }
        if (maxWithdraw(_owner) < _assets) {
            revert UnsufficientBalance();
        }
    }
```

However, neither function `withdraw()` nor `_beforeWithdraw()` (which is called inside `withdraw()`) decreases the allowance increased by function `deposit()`.
This basically implies, t
Incorrect burning mechanism in `_beforeRedeem()`
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Function `_beforeRedeem()` burns shares twice

## Proof of Concept

[File: PrincipalToken.sol](https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/tokens/PrincipalToken.sol#L812-L820)
```
if (block.timestamp >= expiry) {
            if (!ratesAtExpiryStored) {
                storeRatesAtExpiry();
            }
        } else {
            updateYield(_owner);
            IYieldToken(yt).burnWithoutUpdate(_owner, _shares);
        }
        _burn(_owner, _shares);
```

As demonstrated above, shares are burned twice.
When `block.timestamp < expiry`, then we're entering the `else` branch and execute `IYieldToken(yt).burnWithoutUpdate(_owner, _shares);`.
Then, afterwards, the second burn is being executed: `_burn(_owner, _shares);`

## Tools Used

Manual code review


## Recommended Mitigation Steps

Shares should be burned only once.


## Assessed type

Other
Function `updateYield()` can be called by anyone
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-L365


# Vulnerability details

## Impact

Function `updateYield()` is public and does not implement any check which verifies the caller.
This implies, that anyone can update yield of any user.

## Proof of Concept

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

As demonstrated above, function is public, without any modifier. It does not implement any check which verifies who calls `updateYield()`. Inside this function, there's a call to `PrincipalTokenUtil._computeYield()` - which also, does not perform any caller verification.

This implies, that anyone can call `updateYield()` on any provided `_user` address - thus anyone can update yield of any other user.

## Tools Used

Manual code review


## Recommended Mitigation Steps

Implement correct access control on `updateYield()` function.


## Assessed type

Access Control
TokenizedFee is not applied as expected with the user's discount fee.
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L756-L760
https://github.com/code-423n4/2024-02-spectra/blob/main/src/libraries/PrincipalTokenUtil.sol#L166
https://github.com/code-423n4/2024-02-spectra/blob/main/src/Registry.sol#L115


# Vulnerability details

## Impact
TokenizedFee is not applied as expected with the user's discount fee.

## Proof of Concept
[PrincipalToken.sol::_depositIBT()](https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L750) calls `PrincipalTokenUtil._computeTokenizationFee()` and then it calls `IRegistry(_registry).getFeeReduction(_pt, msg.sender)`.

Issue is here, current implementation passes msg.sender for user address param of `getFeeReduction`, but msg.sender points PT contract's address, but not user's address.
```
    function getFeeReduction(address _pt, address _user) external view override returns (uint256) {
        return feeReduction[_pt][_user];
    }
```
So user can't get benefit of reduced fee provided from this protocol, so It will affect protocol's reputation

This bug will affect to all functions with `PrincipalTokenUtil._computeTokenizationFee()`, like '_previewDepositIBT()`

## Tools Used
Manual Review

## Recommended Mitigation Steps
Pass caller's address into PrincipalTokenUtil._computeTokenizationFee(), and then pass it as second param of IRegistry(_registry).getFeeReduction()
```
        uint256 tokenizationFee = PrincipalTokenUtil._computeTokenizationFee(
            _ibts,
            address(this),
            registry,
=>          msg.sender
        );
.....
.....
    function _computeTokenizationFee(
        uint256 _amount,
        address _pt,
        address _registry,
=>      address _user
    ) internal view returns (uint256) {
        
        return
            _amount
                .mulDiv(IRegistry(_registry).getTokenizationFee(), FEE_DIVISOR, Math.Rounding.Ceil)
                .mulDiv(
                    FEE_DIVISOR - IRegistry(_registry).getFeeReduction(_pt, 
=>                  _user),
                    FEE_DIVISOR,
                    Math.Rounding.Ceil
                );
    }
```


## Assessed type

Error
PrincipalToken.sol:: deposit() with any asset does not account fee-on-transfer token
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
PrincipalToken.sol:: deposit() with any asset does not account fee-on-transfer token like USDT, it causes loss of asset of Principal Token contract.

## Proof of Concept
https://github.com/code-423n4/2024-02-spectra/blob/main/src/tokens/PrincipalToken.sol#L176-L185

example: IBT: aUSDT(Aave Interest bearing USDT token) -- asset: USDT

User can deposit FOT token without fee, there are some fee-on-transfer tokens like USDT.
Here user transfers any amount of FOT asset into PT contract, then received amount is less than initial amount transfered from user because of transfer fee.
But PT deposits amount which is not accounted for fee, into IBT vault
So It will cause loss of PT asset balance.

## Tools Used
Manual Review

## Recommended Mitigation Steps
Check PT's asset balance before/after transfer, so exact received amount should be deposited into Vault


## Assessed type

Token-Transfer
Potential incorrect and inconsistent yield computation
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

`PrincipalToken` updates rates of ibt and pt, whenever a user interacts with the contract, which impacts other user's claimed yield. In other words, a user's claimed yield can be different depending on other user's activity even for the same market status. The yield computation should be deterministic when a market status is same, rather than being impacted by a random user's activity. This leads to an unreliable protocol. Therefore, the impact of this issue is high.

## Proof of Concept

The following PoC demonstrates the concept. Due to userTwo's activity (see highlighted), userOne can get non-zero yield. If there was no userTwo's activity, there will be no yield for userOne.

```diff
    function testInconsistentYield() public {
        uint256 assetAmount = 1 ether;

        address userOne = makeAddr("userOne");
        address userTwo = makeAddr("userTwo");
        address userThree = makeAddr("userThree");

        underlying.mint(userOne, assetAmount);
        underlying.mint(userTwo, assetAmount);
        underlying.mint(userThree, assetAmount);

        /* deposit */
        vm.startPrank(userOne);
        underlying.approve(address(principalToken), assetAmount);
        principalToken.deposit(assetAmount, userOne);
        vm.stopPrank();

        vm.startPrank(userTwo);
        underlying.approve(address(principalToken), assetAmount);
        principalToken.deposit(assetAmount, userTwo);
        vm.stopPrank();

        /* ibtRate changes by -50% */
        _increaseRate(-50); 

        /* yt transfer from userTwo to userThree */
        vm.prank(userTwo);
@>>     yt.transfer(userThree, assetAmount);

        /* ibtRate back to original */
        _increaseRate(100);

        /* userOne claims */
        vm.prank(userOne);
        uint256 yieldUserOne = principalToken.claimYield(userOne);

        /* userOne got yield! */
        assertEq(yieldUserOne, 5e17);
    }
```

## Tools Used

VS Code, Foundry

## Recommended Mitigation Steps

The yield calculation would need to be more deterministic and consistent. One method would be that the protocol updates rates periodically (e.g., every day) instead of relying soley on users' activities.


## Assessed type

Other
The claimed yield of a user is impacted by other users' activity and the yield computation could be inconsistent.
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2024-02-spectra/blob/383202d0b84985122fe1ba53cfbbb68f18ba3986/src/libraries/PrincipalTokenUtil.sol#L55


# Vulnerability details

## Summary

The yield generated for YT holders is supposed to be influenced by market rates, but the current yield computation algorithm is also impacted by other users' activity, allowing a user to receive a positive yield even if the effective rate remains the same.

## Vulnerability Details

The `PrincipalToken` contract's `ptRate` and `ibtRate` are updated when any user interacts with the protocol. In other words, `ptRate` and `ibtRate` are not updated in a systematic or periodic manner, but rather by user activities which are non-deterministic. This leads to inconsistent yield computation by `PrincipalTokenUtil::_computeYield`. Furthermore, the computation only accounts for relative differences in old rates and new rates and does not consider the original yield at YT mint for a specific user, resulting in yield generation even when the rate did not, in fact, increase.

## Impact

The impact of this vulnerability is HIGH because yield computation is one of the fundamental aspects of the protocol. Incorrect or inconsistent yield computation impacts all protocol users.

## Proof of Concept

The following test codes can be added to `ContractPrincipalToken1` to prove the concept. `testClaimOtherUserImpact` shows the impact of other users' activity (e.g., yield claim) giving unintended yield, while `testClaimNoOtherUserImpact` shows what happens when there is no other user activity. In both cases, the market ibtRate has decreased and increased back to the original value. From the perspective of MOCK_ADDR_1, the market rate did not change, but MOCK_ADDR_1 still earns yield in the `testClaimOtherUserImpact` case because another user (MOCK_ADDR_2) interacted with the protocol when the ibtRate decreased. For the same market situation, however, MOCK_ADDR_1 does not earn any yield in `testClaimNoOtherUserImpact`. The claimed yields are shown in the codes marked with `@>`.


```solidity
    function testClaimOtherUserImpact() public {
        uint256 amount = 1e18; 

        underlying.mint(MOCK_ADDR_1, amount);
        underlying.mint(MOCK_ADDR_2, amount);

        // MOCK_ADDR_1 & 2 deposit assets to PT
        vm.startPrank(MOCK_ADDR_1);
        underlying.approve(address(principalToken), amount);
        principalToken.deposit(amount, MOCK_ADDR_1);
        vm.stopPrank();

        vm.startPrank(MOCK_ADDR_2);
        underlying.approve(address(principalToken), amount);
        principalToken.deposit(amount, MOCK_ADDR_2);
        vm.stopPrank();

        // decreased ibtRate, ADDR_2 claims
        _increaseRate(-50); 
        vm.prank(MOCK_ADDR_2);
        uint256 yieldClaimedADDR_2 = principalToken.claimYield(MOCK_ADDR_2);
        console2.log("@test: yieldClaimedADDR_2 v1 = ", yieldClaimedADDR_2); 

        // ibtRate back to original, ADDR_1 claims
        _increaseRate(100);
A momentary de-pegging can result in a loss for all PT holders, although the market recovers while the protocol could "earn" from the de-pegging.
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Summary

The `ptRate` and `ibtRate` are maintained and updated at the PT contract level, and they are updated not systematically but rather only when any user interacts with the contract. Furthermore, ptRate is only allowed to decrease, which makes it unfavorable for all users in the event of any (momentary) past de-pegging event regardless of the current situation.

## Vulnerability Details

In the protocol, PTs are supposed to be redeemable 1:1 with the underlying assets except when in the (continued) negative yield case. However, the `ptRate` is only allowed to decrease even if the market recovers from a (momentary) de-pegging event. Furthermore, the PT contract's `ptRate` and `ibtRate` are updated only when any user interacts with the protocol. This poses a risk for loss of asset balances for every user of the protocol if any user interacts with the protocol when `ibtRate` is decreased. Even if the market recovers (e.g., `ibtRate` increased back), the users will never redeem the fair amount of assets back, and all the loss of users will be taken as the "gain" of the protocol. This issue is especially concerning because the asset loss of all users can be caused by a single user/actor who interacts with the protocol.

## Impact

The impact of this vulnerability is HIGH because this can impact ALL users. Furthermore, the longer a user keeps PTs with the protocol, the higher chance that the user can get the loss.

## Proof of Concept

The following test code can be added to `ContractPrincipalToken1`. Only two user cases are shown here for simplicity, but it applies to any number of users. The code proves that any de-pegging event can cause the loss for users even if the market recovers, and the loss of users means the gain of the protocol. The brief flow for the code is as follows:

1. `ADDR_1` deposits assets to the PT contract.
2. `ADDR_2` deposits assets when `ibtRate` has decreased.
3. The market has recovered, and `ADDR_1` redeems assets when `ibtRate` has recovered.
    - `ADDR_1` only receives a reduced amount of assets because of step 2.
    - The loss of `ADDR_1` reflected in the gain of the protocol.

```solidity
    function testDepegCauseUserLoss() public {
        // Amount of asset to deposit
        uint256 amount = 1e18; 

        // Equip ADDR_1/2 with assets
        underlying.mint(MOCK_ADDR_1, amount);
        underlying.mint(MOCK_ADDR_2, amount);

        // 1. MOCK_ADDR_1 deposits to PT
        vm.startPrank(MOCK_ADDR_1);
        underlying.approve(address(principalToken), amount);
        principalToken.deposit(amount, MOCK_ADDR_1);
        vm.stopPrank();

        // Check (redeemable) asset balance
        uint256 ptBalanceOfADDR_1 = principalToken.balanceOf(MOCK_ADDR_1);
        uint256 assetBalanceOfADDR_1 = principalToken.convertToUnderlying(ptBalan
Inconsistent ERC-4626 asset() Function Response
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The `PrincipalToken` contract is designed to interact with any ERC-4626 compliant vault and expects the `asset()` function in `ERC4626` to return an address, but instead the function returns an ERC-20 token contract, this can lead to integration failures. Approximately 20% of vaults may be vulnerable to this issue, potentially resulting in transaction reversion, loss of funds, and a degraded user experience.

## Proof of Concept

Resource : https://medium.com/@vikram.arun/multichain-erc-4626-conformance-f34b682b273b go to the section Incorrect Return Types.

Deploy a `PrincipalToken` contract designed to interact with `ERC-4626` vaults.
Call the `asset` function on a variety of ERC-4626 vaults.
```javascript
        // @audit not every vault returns address
        _asset = IERC4626(_ibt).asset();
```
Observe that in some cases, instead of an address, an ERC-20 token contract is returned.

Attempt to use the returned contract in subsequent operations, such as `withdraw`, `deposit` , `_beforeWithdraw`.
that these operations fail or behave unexpectedly due to the incorrect return type.


## Tools Used
Manual review and some Research


## Recommended Mitigation Steps
Explicitly Check Return Type: When interacting with an ERC-4626 vault, verify that the asset() function returns an address. You can add checks in your contract to ensure that the returned value is a valid address.
```diff
function initialize(
        address _ibt,
        uint256 _duration,
        address _initialAuthority
    ) external initializer {
        if (_ibt == address(0) || _initialAuthority == address(0)) {
            revert AddressError();
        }
        // means that underlying token is zero
        if (IERC4626(_ibt).totalAssets() == 0) {
            revert RateError();
        }
        // address of underlying token of the ibt token ex usdc eth uni link
        _asset = IERC4626(_ibt).asset();
        // Check that the returned value is a valid non-zero address
++      require(_asset != address(0), "Asset address is zero");
       // Check that the address contains code, i.e., it is a contract
++      uint256 size;
++       assembly {
++           size := extcodesize(_asset)
++       }
++        require(size > 0, "Asset address is not a contract");
        duration = _duration;
        expiry = _duration + block.timestamp;
        // for ecample aUSDC stETH
        string memory _ibtSymbol = IERC4626(_ibt).symbol();
        string memory name = NamingUtil.genPTName(_ibtSymbol, expiry);
        __ERC20_init(name, NamingUtil.genPTSymbol(_ibtSymbol, expiry));
        __ERC20Permit_init(name);
        __Pausable_init();
        __ReentrancyGuard_init();
        __AccessManaged_init(_initialAuthority);
        // decimal of the ibt token which is 6 in example of the usdc
        // @audit wha
Yield Computation Reverts When There is Enough Yield
Audit: 2024-02-spectra-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Yield Computation Reverts When There is Enough IBT Rate Positive Yield to cover for PT rate negative yield in the PrincipalTokenUtil.sol contract
## Proof of Concept
```solidity
 function _computeYield(
        address _user,
        uint256 _userYieldIBT,
        uint256 _oldIBTRate,
        uint256 _ibtRate,
        uint256 _oldPTRate,
        uint256 _ptRate,
        address _yt
    ) external view returns (uint256) {
P1->>>        if (_oldPTRate == _ptRate && _ibtRate == _oldIBTRate) {
            return _userYieldIBT;
        }
        uint256 newYieldInIBTRay;
        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);
P2->>>        if (_oldPTRate == _ptRate && _ibtRate > _oldIBTRate) {
            // only positive yield happened
            newYieldInIBTRay = ibtOfPTInRay.mulDiv(_ibtRate - _oldIBTRate, _ibtRate);
        } else {
            if (_oldPTRate > _ptRate) {
                // PT depeg happened
                uint256 yieldInAssetRay;
P3->>>                if (_ibtRate >= _oldIBTRate) {
                    // both negative and positive yield happened, more positive
                    yieldInAssetRay =
                        _convertToAssetsWithRate(
                            userYTBalanceInRay,
                            _oldPTRate - _ptRate,
                            RayMath.RAY_UNIT,
                            Math.Rounding.Floor
                        ) +
                        _convertToAssetsWithRate(
                            ibtOfPTInRay,
                            _ibtRate - _oldIBTRate,
                            RayMath.RAY_UNIT,
                            Math.Rounding.Floor
                        );
 P4->>>               } 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;