xanoutas/solidity-security-findings huggingface dataset:
Vesting vests too much when `initialUnlock` is used
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/tokens/Vesting.sol#L273-L274


# Vulnerability details

## Description
The vesting contract in tap-token has a feature to release a certain portion of amount directly called `initialUnlock`.

Here is how it is calculated when querying how much is vested at a point in time, [`Vesting::_vested`](https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/tokens/Vesting.sol#L273-L274):
```solidity
File: tap-token/contracts/tokens/Vesting.sol

273:        _start = _start - __initialUnlockTimeOffset; // Offset initial unlock so it's claimable immediately
274:        return (_totalAmount * (block.timestamp - _start)) / _duration; // Partially vested
```

Unfortunately, there's an issue. The contract simply offsets the start time with the initial `__initialUnlockTimeOffset` (`initialUnlock`). Hence at the end of the vesting period, `block.timestamp - start` can become larger than `_duration`, vesting a larger amount than intended.

This will reset as soon as the `_duration` is reached so it requires a user to claim just before the `_duration` has reached its end.

## Impact
Vestors in pools with `initialUnlocks` will be able to claim more vested than intended. This will impact vestors who claim later as there might not be enough tokens in the contract to cover their withdrawals.

## Proof of Concept
Test in `tap-token/test/Vesting.t.sol`:
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.22;

import "forge-std/Test.sol";
import "forge-std/console.sol";
import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

import {Vesting} from "tap-token/tokens/Vesting.sol";

contract MockToken is ERC20 {
    constructor() ERC20("Mock","Token") {}

    function mint(address to, uint256 amount) public {
        _mint(to,amount);
    }
}

contract VestingTest is Test {

    MockToken token = new MockToken();

    Vesting vesting;

    address ada = makeAddr("ada");

    function setUp() public {
        vesting = new Vesting({
            _cliff: 0,
            _duration: 100,
            _owner: address(this)
        });

        vm.warp(1000);
    }

    function testInitalUnlock() public {
        vesting.registerUser(ada, 100);

        token.mint(address(vesting),100);
        vesting.init({
            _token: IERC20(address(token)),
            _seededAmount: 100,
            _initialUnlock: 1_000
        });

        // one second before finish
        // ada has received more than intended amount
        vm.warp(1099);
        assertEq(vesting.vested(ada),109);
    }
}
```

## Tools Used
Manual audit

## Recommended Mitigation Steps
Consider rethinking the method of providing the `initialUnlock`, if you either want a less steep slope or a non-vesting period until it should start to vest again.


## Assessed type

Math
AirdropBroker / TapiocaOptionBroker cannot ensure that paymentTokenValuation's decimals in _getDiscountedPaymentAmount is 18
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/options/TapiocaOptionBroker.sol#L579
https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/option-airdrop/AirdropBroker.sol#L525


# Vulnerability details

Despite the developer comments indicating that the decimals should be all 18, but we can find some real ITapiocaOracle implementations in the https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/oracle/implementation/ . Them shows that the decimals of oracle USD prices returned from ITapiocaOracle implementations can be 30 https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/oracle/implementation/Arbitrum/GLPOracle.sol#L26,
```solidity
    function decimals() external pure returns (uint8) {
        return 30;
    }
```
 or dynamic setting https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/oracle/SeerUniSolo.sol#L21 :
```
    constructor(
        ...
        uint8 _decimals,
        OracleUniSoloConstructorData memory _oracleUniSoloConstructorData
    ) OracleUniSolo(_oracleUniSoloConstructorData) {
        ...
        decimals = _decimals;
``` 

## Impact
We can find the logic for calculating the payment token amount based on the oracle USD value in the `_getDiscountedPaymentAmount` method:
```
uint256 rawPaymentAmount = _otcAmountInUSD / _paymentTokenValuation;
```
So if the USD values from different oracles have different decimals like they are in the above implementations, the rawPaymentAmount will be scaled by 10 ** the difference in their decimals. 

## Recommendation
 Read the actual decimals() from the oracle and scale both _otcAmountInUSD and _paymentTokenValuation to 1e18 as USD values.


## Assessed type

Decimal
Approval for pearlmit is missing before repaying in the `depositRepayAndRemoveCollateralFromMarket` function of MagnetarAssetModule 
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarAssetModule.sol#L99-L106


# Vulnerability details

## Description
`depositRepayAndRemoveCollateralFromMarket` function attempts to repay the user's loan in the market. In the case of `data.depositAmount` > 0, it will use the assets existing in the contract (which was deposited by user before) to repay.
```solidity=
// @dev performs a repay operation for the specified market
if (data.repayAmount > 0) {
    _setApprovalForYieldBox(data.market, _yieldBox);
    (Module[] memory modules, bytes[] memory calls) = IMarketHelper(data.marketHelper).repay(
        data.depositAmount > 0 ? address(this) : data.user, data.user, false, data.repayAmount
    );
    _market.execute(modules, calls, true);
    _revertYieldBoxApproval(data.market, _yieldBox);
}
```
However, the `Singularity.repay()` function will pull YieldBox shares of the asset by using the pearlmit contract. Here is the code snippet of Singularity in the tapioca-bar gitmodule.
```solidity=
///https://github.com/Tapioca-DAO/Tapioca-bar/blob/0dca4a0b8e31b7aa59cb20540079b80a95bbe494/contracts/markets/singularity/SGLLendingCommon.sol
function _repay(address from, address to, bool skim, uint256 part) internal returns (uint256 amount) {
    ...

    uint256 share = yieldBox.toShare(assetId, amount, true);
    uint128 totalShare = totalAsset.elastic;
    _addTokens(from, to, assetId, share, uint256(totalShare), skim);
    ...
}
```

```solidity=
///https://github.com/Tapioca-DAO/Tapioca-bar/blob/0dca4a0b8e31b7aa59cb20540079b80a95bbe494/contracts/markets/singularity/SGLCommon.sol#L165-L177
function _addTokens(address from, address, uint256 _assetId, uint256 share, uint256 total, bool skim) internal {
    if (skim) {
        if (share > yieldBox.balanceOf(address(this), _assetId) - total) {
            revert TooMuch();
        }
    } else {
        // yieldBox.transfer(from, address(this), _assetId, share);
        bool isErr = pearlmit.transferFromERC1155(from, address(this), address(yieldBox), _assetId, share);
        if (isErr) {
            revert TransferFailed();
        }
    }
}
```
Because the `depositRepayAndRemoveCollateralFromMarket` function doesn't approve YieldBox's shares of the asset for the pearlmit contract of the market, pearlmit will be unable to pull shares from this contract. Therefore, it will revert when repaying in a Singularity market
## Impact
`depositRepayAndRemoveCollateralFromMarket` will be broken
## Tools Used
Manual review
## Recommended Mitigation Steps
Should approve YieldBox shares of asset for pearlmit before repaying.


## Assessed type

Other
`exitPositionAndRemoveCollateral` function of MagnetarOptionModule calls `removeAsset()` with the wrong value
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarOptionModule.sol#L151-L158


# Vulnerability details

## Description
`exitPositionAndRemoveCollateral` function attempts to remove asset of user from Singularity market.
```solidity=
function exitPositionAndRemoveCollateral(ExitPositionAndRemoveCollateralData memory data) public payable {
    ...
    
    if (data.removeAndRepayData.removeAssetFromSGL) {
        uint256 _assetId = singularity_.assetId();
        uint256 share = yieldBox_.toShare(_assetId, _removeAmount, false);

        address removeAssetTo = data.removeAndRepayData.assetWithdrawData.withdraw
            || data.removeAndRepayData.repayAssetOnBB ? address(this) : data.user;

        singularity_.removeAsset(data.user, removeAssetTo, share);
        
        ...
}
```

It calls the removeAsset function of Singularity with the share variable, which represents the YieldBox shares of the repaid asset amount. However, the `removeAsset` function of Singularity uses a fraction of the lended part to be the repaid variable (see [code snippet](https://github.com/Tapioca-DAO/Tapioca-bar/blob/0dca4a0b8e31b7aa59cb20540079b80a95bbe494/contracts/markets/singularity/SGLCommon.sol#L199-L216) from gitmodule tapioca-bar).
```solidity=
///https://github.com/Tapioca-DAO/Tapioca-bar/blob/0dca4a0b8e31b7aa59cb20540079b80a95bbe494/contracts/markets/singularity/Singularity.sol#L245-L253
function removeAsset(address from, address to, uint256 fraction)
    external
    optionNotPaused(PauseType.RemoveAsset)
    returns (uint256 share)
{
    _accrue();
    share = _removeAsset(from, to, fraction);
    _allowedLend(from, share);
}

```
```solidity=
///https://github.com/Tapioca-DAO/Tapioca-bar/blob/0dca4a0b8e31b7aa59cb20540079b80a95bbe494/contracts/markets/singularity/SGLCommon.sol#L199-L216
function _removeAsset(address from, address to, uint256 fraction) internal returns (uint256 share) {
    if (totalAsset.base == 0) {
        return 0;
    }
    Rebase memory _totalAsset = totalAsset;
    uint256 allShare = _totalAsset.elastic + yieldBox.toShare(assetId, totalBorrow.elastic, false);
    share = (fraction * allShare) / _totalAsset.base;

    _totalAsset.base -= fraction.toUint128();
    ...
```

Due to removing with a parameter being the share amount (YieldBox shares of asset tokens), the contract will remove more assets than needed from the user in `exitPositionAndRemoveCollateral`. This results in the user losing funds when using this functionality because `removeAssetTo` can be this contract if the user attempts to withdraw after removal:
```solidity=
address removeAssetTo = data.removeAndRepayData.assetWithdrawData.withdraw
            || data.removeAndRepayData.repayAssetOnBB ? address(this) : data.user;
```

For example, after depositing and accruing interest in the Singularity contract, Bob now has 100 asset fractions (base) and 1000 YieldBox shares of the
The introduction of `__initialUnlockTimeOffset` may allow the attacker to vest more tokens than anticipated
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/tokens/Vesting.sol#L273-L274


# Vulnerability details

## Description
The variable `__initialUnlockTimeOffset` is introduced to facilitate the initial unlock process. It's computed within the `Vesting.init()` function and later utilized in the `Vesting._vested()` function to adjust the start time, allowing users to claim their vested amounts immediately.

```solidity
function _vested(uint256 _totalAmount) internal view returns (uint256) {
    uint256 _cliff = cliff;
    uint256 _start = start;
    uint256 _duration = duration;

    if (_start == 0) return 0; // Vesting not initiated

    if (_cliff > 0) {
        _start = _start + _cliff; // Apply cliff offset
        if (block.timestamp < _start) return 0; // Cliff period not reached
    }

    if (block.timestamp >= _start + _duration) return _totalAmount; // Fully vested

    _start = _start - __initialUnlockTimeOffset; // Adjust initial unlock time for immediate claiming
    return (_totalAmount * (block.timestamp - _start)) / _duration; // Calculate partially vested amount
}
```

However, the calculation performed by the function above is accurate only when `block.timestamp == _start`. To illustrate this point, consider the following scenario:

1. The Vesting contract's state is as follows:
   - `cliff = 0`
   - `start = 100`
   - `duration = 100`
   - `user[Alice].amount = 50`
   - The initial unlock `_initialUnlock = 30`. After the `init()` function is executed, the value of `__initialUnlockTimeOffset` is calculated as `100 - (100 - 30 * 100 / 100) = 30`.

2. At `block.timestamp = 190`, Alice calls the `Vesting.claim()` function to claim her assigned amount. Subsequently, the `vested(user[Alice].amount = 50)` function is invoked to compute Alice's vested amount.
   
   ```
   vested(_totalAmount = 50)
       line 273: 
            _start = _start - __initialUnlockTimeOffset = 100 - 30 = 70
       line 274: 
            return 
               (_totalAmount * (block.timestamp - _start)) / _duration
               =   (50 * (190 - 70)) / 100 
               =   60
   ```

As observed in the example above, Alice is able to claim 60 tokens, which exceeds her assigned amount by more than 10 tokens.

## Impact
The attacker can claim more tokens than the amount assigned, leading to an insufficient amount of tokens available for other users to claim.

## Tools Used
Manual review

## Recommended Mitigation Steps
Consider eliminating the usage of the variable `__initialUnlockTimeOffset` and instead introducing a new variable called `initialUnlockAmount`. Implement a new function allowing users to claim this amount at any time.

```solidity
function claimInitialUnlockAmount() external {
    uint userClaimedAmount = initialUnlockAmount * users[msg.sender].amount / totalRegisteredAmount; 
    token.safeTransfer(msg.sender, userClaimedAmount);
}
```


## Assessed type

Math
The attacker can steal the reward from twTAP by utilizing the function `TapTokenReceiver::_claimTwpTapRewardsReceiver()`
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/tokens/TapTokenReceiver.sol#L156


# Vulnerability details

## Description
The function `twTAP.claimRewards(_tokenId, _to)` serves to claim all rewards distributed to the position identified by `_tokenId`.

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/governance/twTAP.sol#L396-L404
```solidity=
function claimRewards(uint256 _tokenId, address _to)
    external
    nonReentrant
    whenNotPaused
    returns (uint256[] memory amounts_)
{
    _requireClaimPermission(_to, _tokenId);
    amounts_ = _claimRewards(_tokenId, _to);
}
```

This function initiates by verifying whether `_to` possesses the Claim permission for the `_tokenId` before proceeding with the reward claim and transfer to `_to`. In simpler terms, before calling the function, the owner of `_tokenId` must grant allowance of `_tokenId` to the address `_to`.

Applying the same principle to the function `TapTokenReceiver::_claimTwpTapRewardsReceiver()`, utilized for claiming the reward for the token's owner and transferring it to the receiver through a cross-chain message, it is necessary for the owner of the `twTAP` with `claimTwTapRewardsMsg_.tokenId` to grant an allowance to the `TapToken` contract prior to invoking this function.

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/tokens/TapTokenReceiver.sol#L152-L203
```solidity=
function _claimTwpTapRewardsReceiver(bytes memory _data) internal virtual twTapExists {
    ... 
    
    // Claim rewards, make sure to have approved this contract on TwTap.
    uint256[] memory claimedAmount_ = twTap.claimRewards(claimTwTapRewardsMsg_.tokenId, address(this));
    
    ... 
    
        TapTokenSender(rewardToken_).sendPacket{
            value: claimTwTapRewardsMsg_.sendParam[sendParamIndex].fee.nativeFee
        }(claimTwTapRewardsMsg_.sendParam[sendParamIndex], bytes(""));
    
    ... 
```

However, a vulnerability arises when this internal function can be triggered by anyone, not just the token's owner, through the cross-chain message. An attacker could preempt the owner's cross-chain transaction, create another cross-chain message using `_msgType = MSG_CLAIM_REWARDS`, and set the receiver of the reward tokens to their own address. Since the Claim permission has been granted to the `TapToken` contract, it would still proceed to claim the reward on behalf of the owner but inadvertently transfer it to the attacker's address instead.

## Impact
The rewards from twTAP are susceptible to theft

## Tools Used
Manual review

## Recommended Mitigation Steps
Consider verifying that the sender on the source chain of this transaction is also approved by the owner of the `tokenId`


## Assessed type

Timing
[H8] Incorrect approval mechanism breaks all Magnetar functionality
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/Magnetar.sol#L1-L354


# Vulnerability details

## Impact

The Magnetar contract hands out approvals to various contracts so that the target contracts can use any tokens held currently by the Magnetar contract.

The issue is that at some point of time, all the target contracts were refactored to use permitC to handle token transfers. However this change wasn't reflected in the Magnetar contracts. Thus instead of handing out permitC approvals, Magnetar hands out normal ERC20 approvals or yieldbox approvals. This essentially breaks the whole system.

There are numerous instances of this in the codebase. Essentially almost every approval in the Magnetar contract is incorrect. Below are some examples, however the entire codebase needs to be checked for approvals and corrected.

The `_depositYBLendSGL` function in `MagnetarAssetCommonModule.sol` contract gives approval to the singularity contract via yieldbox. However if we check the `_addTokens` function in the singularity contract below, we see the token transfers actually take place via pearlmit / permitC.

```solidity
_setApprovalForYieldBox(singularityAddress, yieldBox_);
```

https://github.com/Tapioca-DAO/Tapioca-bar/blob/9d76b2fc7e2752ca8a816af2d748a0259af5ea42/contracts/markets/singularity/SGLCommon.sol#L165-L177

Since the Magnetar contract does not give permitC approval to the singularity contract, and instead only gives yieldbox approval, the singularity contract is unable to transfer tokens from the Magnetar contract.

Similarly, in the `_wrapSglReceipt` function, the Magnetar gives approval to the TOFT contract vie ERC20 approval.

```solidity
IERC20(sgl).approve(tReceiptAddress, fraction);
```

But if we check the TOFT contract, we see the tokens are transferred via permitC and not with the raw tokens.

https://github.com/Tapioca-DAO/TapiocaZ/blob/57750b7e997e5a1654651af9b413bbd5ea508f59/contracts/tOFT/BaseTOFT.sol#L73

Since the Magnetar contract does not hand out the permitC approvals, most of the token transfers via Magnetar will fail.

## Proof of Concept

The issue arises due to the target contracts using permitC, while Magnetar only giving approvals of the token itself or yieldbox. This can be verified by checking the Magnetar contract and the target contracts as shown above

## Tools Used

Manual Review

## Recommended Mitigation Steps

Refactor Magnetar to give approvals via permitC throughout.



## Assessed type

Token-Transfer
[H7] Magnetar's `_withdrawToChain` function can be abused to drain tokens and positions
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarBaseModule.sol#L51-L84


# Vulnerability details

## Impact

The function `_withdrawToChain` is used by the magnetar contracts to send cross chain messages. This function does 2 different calls, depending on the inputs.

```solidity
if (data.unwrap) {
            _lzCustomWithdraw(
                //...
            );
        } else {
            _lzWithdraw(asset, data.lzSendParams, data.sendGas, data.sendVal);
        }
```

The `_lzWithdraw` function is safe, since it prepares a payload using `_prepareLzSend`. This helper function populates a call.

```solidity
prepareLzCallReturn = _toeHelper.prepareLzCall(
    ITapiocaOmnichainEngine(_asset),
    PrepareLzCallData({
        dstEid: _lzSendParam.sendParam.dstEid,
        recipient: _lzSendParam.sendParam.to,
        amountToSendLD: _lzSendParam.sendParam.amountLD,
        minAmountToCreditLD: _lzSendParam.sendParam.minAmountLD,
        msgType: 1, // SEND
        composeMsgData: ComposeMsgData({
            index: 0,
            gas: 0,
            value: 0,
            data: bytes(""),
            prevData: bytes(""),
            prevOptionsData: bytes("")
        }),
        lzReceiveGas: _lzSendGas,
        lzReceiveValue: _lzSendVal
    })
);
```

Crucially, the `composeMsgData` is set to an empty call. However, in the `_lzcustomWithdraw` function, the `composeMsgData` is set by the caller.

```solidity
PrepareLzCallReturn memory prepareLzCallReturn2 = _toeHelper.prepareLzCall(
    ITapiocaOmnichainEngine(_asset),
    PrepareLzCallData({
        dstEid: _lzSendParam.sendParam.dstEid,
        recipient: _lzSendParam.sendParam.to,
        amountToSendLD: 0,
        minAmountToCreditLD: 0,
        msgType: _lzComposeMsgType,
        composeMsgData: ComposeMsgData({
            index: 0,
            gas: _lzComposeGas,
            value: prepareLzCallReturn.msgFee.nativeFee.toUint128(),
            data: _lzSendParam.sendParam.composeMsg,
            prevData: bytes(""),
            prevOptionsData: bytes("")
        }),
        lzReceiveGas: _lzSendGas + _lzComposeGas,
        lzReceiveValue: _lzComposeVal
    })
```

Here the `composeMsgData` is set to `_lzSendParam.sendParam.composeMsg`, which is set by the user, and there are **no checks** on the content of this message. This can wreak havoc in the system.

Assuming the target of the cross-chain call is a TOFT contract (examples of such will be shown later), this is the flow of operations:

```mermaid
  flowchart LR
      User --> MA["Magnetar\n(chain A)"];
      MA -- sendPacket() --> TOFTA["TOFT\n(chain A)"];
      TOFTA -- lzSend --> EA["Endpoint\n(chain A)"];
      EB["Endpoint\n(chain B)"] -- lzReceive (1) --> TOFTB["TOFT\n(chain B)"];
      TOFTB -- sendCompose (2) --> EB;
      EB -- lzcompose (3) --> TOFTB;
```

The crucial point to note is that the Magnetar calls the TOFT token contract.