xanoutas/solidity-security-findings huggingface dataset:
[H5] `_processOFTOperation` in `Magnetar.sol` has absolutely no checks
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The function `_processOFTOperation` in the Magnetar contract is intended to be called for crosschain oft calls. However in the actual implementation, there are absolutely no checks at all except a whitelist check.

```solidity
if (!cluster.isWhitelisted(0, _target)) revert Magnetar_NotAuthorized(_target, _target);
    _executeCall(_target, _actionCalldata, _actionValue, _allowFailure);
```

The magnetar contract is expected to have user approvals to markets as well as approvals in pearlmit and thus NFTs and tokens. Since there are no checks here, the user can call any operation on any contract as long as it is whitelisted. This is a major security issue since tokens can be easily stolen and positions can be easily manipulated.

As an example, the market and pearlmit contracts are included in the cluster whitelist. Thus any user can use this function to mint USDO tokens from bigbang market or transfer out tokens via pearlmit. This is prevented in other function via the `_checkSender` function which is absent here. So users can fully destroy the system by exploiting the Magnetar contract's approval.

The only thing they need to do is pass the `_action.id == MagnetarAction.OFT` check. So users can set this action id and call burst and steal everything in the system

## Proof of Concept

No POC provided since the absence of checks is evident from the code, and example exploit scenarios are given above.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Build a second whitelist for the `_processOFTOperation` function. Also limit the function signatures that can be called via this. This is an extremely dangerous function and should ideally be dropped from the contract.



## Assessed type

Access Control
[H4] Users can mess with other users' option positions via Magnetar
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The function `_processTapTokenOperation` in the Magnetar.sol contract can be called by users to do operations on their option positions and interact with the tolp or tob contracts. For normal operations, the target contracts (toB and toLP) check the approvals given to `msg.sender`, so the users will first need to give NFT approvals to the Magnetar contract.

The issue is that any user can call these function and mess with the positions of other users due to no checks on the actual `msg.sender`.

```solidity
if (
    funcSig == ITapiocaOptionBroker.exerciseOption.selector
        || funcSig == ITapiocaOptionBroker.participate.selector
        || funcSig == ITapiocaOptionBroker.exitPosition.selector
        || funcSig == ITapiocaOptionLiquidityProvision.lock.selector
        || funcSig == ITapiocaOptionLiquidityProvision.unlock.selector
) {
    _executeCall(_target, _actionCalldata, _actionValue, _allowFailure);
```

As seen above, a variety of functions can be called on the target contracts. The only check done here is that the target must be whitelisted, which it is. In the target contracts, the checks look like this.

```solidity
if (!tOLP.isApprovedOrOwner(msg.sender, _tOLPTokenID)) {
    revert NotAuthorized();
}
```

However, in this context, the `msg.sender` is the magnetar contract, who is supposed to have access under normal operations. The actual caller is never checked, so any user can call these functions and mess with the positions of other users. This is a high severity issue since users can lock up tolp tokens and liquidity and in general mess with operations and even steal tokens via the `unlock` call.

## Proof of Concept

The situation can be recreated via the following steps:

1. Alice gives Magnetar contract access to her NFTs and tokens via pearlmit.
2. Bob calls `unlock` via magnetar and passes in their own address as the `to` address for receiving the liquidity tokens, and passes in Alice's tokenId.
3. TOLP contract checks `msg.sender` which is the magnetar contract. Does not stop the transaction.
4. Bob receives Alice's liquidity tokens.

## Tools Used

Manual Review

## Recommended Mitigation Steps

`_processTapTokenOperation` should check the access of the actual caller, which is the `msg.sender` at this point.



## Assessed type

Access Control
[H3] Tap rewards can be stolen via crosschain calls
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

The function `_claimTwpTapRewardsReceiver` in the TapTokenReceiver.sol contract is used to claim rewards for any Tap position and send it forward to some address. To use this function, the users have to give the Tap token contract approval to use the tap position NFT, which is checked in the `twTAP.sol` contract.

```solidity
if (!_isApprovedOrOwner(_to, _tokenId) && !isERC721Approved(_ownerOf(_tokenId), _to, address(this), _tokenId)) {
    revert NotApproved(_tokenId, msg.sender);
}
```

In the `_claimTwpTapRewardsReceiver` function, the TAP contract calls the `twTAP` contract, and passes in its own address as the `_to` parameter.

```solidity
uint256[] memory claimedAmount_ = twTap.claimRewards(claimTwTapRewardsMsg_.tokenId, address(this));
```

So it is expected that users will have given access to the TAP token contract since that access is the thing that is being checked. Finally, these rewards are passed crosschain to the address specified in `claimTwTapRewardsMsg_.sendParam[sendParamIndex].sendParam.to`.

The issue is that the caller is never checked. The caller of this function on the origin chain is available in the `_srcChainSender` variable, but is never checked. So any user can do this crosschain call, set any address as the recipient and set the target `tokenId` as the `tokenId` of any NFT which has approvals given to the TAP contract (which is what is expected to be the case under normal operations). Thus any user can claim and steal away rewards of any other user's NFT due to the absent checks on `_srcChainSender`.

The only check is done in the `twTAP` contract on the `_to` address, but due to how the function call happens, this `_to` address is the TAP token itself and is thus expected to have the necessary allowance.

## Proof of Concept

Can be recreated via the following steps.

1. Alice creates a twTAP position. ALICE gives the TAP contract address approval to use the NFT in order to collect rewards.
2. BOB sends a cross-chain lz message with `_msgType==MSG_CLAIM_REWARDS` and passes in ALICE's NFT `tokenId`, and their own address as the target address.
3. Since ALICE has given approval to the TAP contract, the TAP contract collects the rewards and sends it off to BOB. BOB's access is never checked.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Check the access of `_srcChainSender`. Make sure that the address is either the owner or has approval in the NFT contract.



## Assessed type

Access Control
exitPositionAndRemoveCollateral() removeAsset amount error
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details
in `MagnetarOptionModule.exitPositionAndRemoveCollateral()`
The user can specify `data.removeAndRepayData.removeAmount` to represent the quantity of assets' to be removed.

The code is as follows:

```solidity
    function exitPositionAndRemoveCollateral(ExitPositionAndRemoveCollateralData memory data) public payable {
...
        // if `data.removeAndRepayData.removeAssetFromSGL` performs the follow operations:
        //      - removeAsset from SGL
        //      - if `data.removeAndRepayData.assetWithdrawData.withdraw` withdraws by using the `withdrawTo` operation
        uint256 _removeAmount = data.removeAndRepayData.removeAmount;
        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);

            //withdraw
            if (data.removeAndRepayData.assetWithdrawData.withdraw) {
                uint256 computedAmount = yieldBox_.toAmount(_assetId, share, false);
                data.removeAndRepayData.assetWithdrawData.lzSendParams.sendParam.amountLD = computedAmount;
                data.removeAndRepayData.assetWithdrawData.lzSendParams.sendParam.minAmountLD = computedAmount;
                _withdrawToChain(data.removeAndRepayData.assetWithdrawData);
            }
        }
...
```

The above code, calling `singularity_.removeAsset(,,share)` is wrong.
The third parameter is `fraction`, but at present, `share` are passed in.

```solidity
interface ISingularity is IMarket {
    function removeAsset(address from, address to, uint256 fraction) external returns (uint256 share);
...

```

The correct way is to convert `shares` into `fraction` first, then pass in `removeAsset()`.

## Impact

The expected number of removeAsset () removals is large.

## Recommended Mitigation

convert `shares` into `fraction` first,  then pass in `removeAsset()`.


## Assessed type

Context
_vested() cliff caused fail to be fully vested on time
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details
The Vesting._vested() method is use to calculate the maximum claimable amount for the user.
If `cliff` exists, if the past time is less than `cliff`, it should return 0.
```solidity
    function _vested(uint256 _totalAmount) internal view returns (uint256) {
        uint256 _cliff = cliff;
        uint256 _start = start;
        uint256 _duration = duration;

        if (_start == 0) return 0; // Not started

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

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

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

The above code, because `_start = _start+_cliff` will be modified, resulting in `Fully vested` being pushed back.
This is unreasonable. The maximum period should be unchanged. 
When `cliff` should only affect the past time < `_cliff` , if it exceeds `_cliff`, the user's due amount should be calculated according to the normal time ratio.

Example:  totalAmount = 500 start =1000 duration = 100  cliff= 10
when block.timestamp = 1020 
_vested should be = 500 * (1020 - 1000) / 100 = 100    (not current impl:  500 * (1020 - 1010) / 100 = 50)

## Impact

Incorrect ` _cliff ` mechanism, resulting in users not being able to ` Fully vested ` at normal time.

## Proof of Concept

## Recommended Mitigation
```diff
    function _vested(uint256 _totalAmount) internal view returns (uint256) {
        uint256 _cliff = cliff;
        uint256 _start = start;
        uint256 _duration = duration;

        if (_start == 0) return 0; // Not started

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

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

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


## Assessed type

Context
_vested() claimable amount calculation error
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability details

The `Vesting._vested()` method is used to calculate the maximum claimable amount for the current user. 
The calculation formula is as follows: `(_totalAmount * (block.timestamp - _start)) / _duration`. 
If there is an `__initialUnlockTimeOffset`, it needs to be subtracted from `_start` before performing the calculation
i.e., `_start = _start - __initialUnlockTimeOffset`.

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

        if (_start == 0) return 0; // Not started

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

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

        _start = _start - __initialUnlockTimeOffset; // Offset initial unlock so it's claimable immediately
        return (_totalAmount * (block.timestamp - _start)) / _duration; // Partially vested
    }
```
The issue with the code snippet above is that the check for being "Fully vested" is incorrect;
 it does not take into account the `__initialUnlockTimeOffset`. 
The correct approach should be:
`if (block.timestamp >= _start - __initialUnlockTimeOffset + _duration) return _totalAmount;// Fully vested`

Resulting in calculations that may be greater than the maximum number `_totalAmount`

Example: 
_totalAmount  =  500 ,  duration = 1000 __initialUnlockTimeOffset = 100 start = 1000  block.timestamp= 1999

 because `block.timestamp < start + duration`  (1999 < 1000 + 1000) it will not return  `Fully vested`

Final calculation result:
start = start - __initialUnlockTimeOffset = 1000 - 100 = 900
return = (_totalAmount * (block.timestamp - _start)) / _duration = 500 * (1999 - 900) / 1000 = 549.5

it is greater  49.5 than the maximum _totalAmount=500

## Impact

Users can `claim' more than they should.

## Recommended Mitigation

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

        if (_start == 0) return 0; // Not started

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

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

        _start = _start - __initialUnlockTimeOffset; // Offset initial unlock so it's claimable immediately
        return (_totalAmount * (block.timestamp - _start)) / _du
Attacker can use MagnetarAction.OFT action of the Magnet to perform operations as any user including directly stealing user tokens
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/Magnetar.sol#L153-L156
https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/Magnetar.sol#L325-L333
https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/MagnetarStorage.sol#L93-L97


# Vulnerability details

This issue requires the combination of two vulnerabilities to achieve the impact described in the title.

The first vuln is that, the `Magnetar._processOFTOperation` function doesn't check the function sigs in the action calldata with the the target addresses. It only ensure the calling target addresses are in the Whitelist of the Cluster. So an attacker can use this vuln to call any whitelist target address from the Magnetar.

The second vuln is that, the Magnetar contract address itself will also be added to the Cluster whitelist. It can be found in the following integration test https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/test_hardhat/magnetar.test.ts#L389 . So if the attacker can let the Magnetar call itself, the msg.sender in the sub-call will be in the whitelist. It will bypass the _checkSender check:
```solidity
    function _checkSender(address _from) internal view {
        if (_from != msg.sender && !cluster.isWhitelisted(0, msg.sender)) {
            revert Magnetar_NotAuthorized(msg.sender, _from);
        }
    }
```

## Impact
Combining the two issues mentioned above, we can carry out the following exploitation. 

1. Call `Magnetar.burst` function with `_action.id == MagnetarAction.OFT`, which will call `_processOFTOperation` function. The `_target` is the Magnetar contract itself, and the _actionCalldata is still an encoded calldata to call the `Magnetar.burst` function with `_action.id == MagnetarAction.OFT` again.
2. In the second call, the msg.sender will be the Magnetar itself, so the from address check in the _checkSender function will be skipped directly because the msg.sender is in the whitelist.
3. Now the attacker can pretend to be anyone and call any contract through the Magnetar. Please note that users approved their tokens to Magnetar if they used it.

## Proof of Concept 
Taking MagnetarAssetModule as an example, it demonstrates how an attacker could steal users' collateral in the market.

Firstly the `_checkSender(data.user);` can be bypassed directly.

And make data.withdrawCollateralParams.withdraw = true, so the collateralWithdrawReceiver will be the Magnetar contract itself.
```
address collateralWithdrawReceiver = data.withdrawCollateralParams.withdraw ? address(this) : data.user;
```
Then if collateralShare > 0, the function will call `_withdrawToChain(data.withdrawCollateralParams);` to withdraw the collateral to the another chain by sending LZ message.

There is no check for withdrawCollateralParams in the `_withdrawToCh
Vesting token can be claimed in excess
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

During vesting initialization (init function), an initial unlock amount can be specified. These tokens can be claimed immediately after the cliff period. 

In order to adapt this one-time unlock to the time curve, the init function calculates the time required to unlock this initial unlock amount based on the duration of the lock, saved as `__initialUnlockTimeOffset`. Then, when calculating the claimable amount, this value will be subtracted from the start:
```solidity
        _start = _start - __initialUnlockTimeOffset; // Offset initial unlock so it's claimable immediately
        return (_totalAmount * (block.timestamp - _start)) / _duration; // Partially vested
```
The issue is that, after sub the `__initialUnlockTimeOffset`, the `block.timestamp - _start` can be greater than `_duration`.

## Impact
This will result in attackers who meet the time conditions claiming an excess amount of tokens. Meanwhile, the last users to claim will be unable to receive tokens due to a shortage, which is tantamount to stealing tokens from the positions of other users.

## Proof of Concept 
Assumption:
- start = X timestamp
- cliff = 1 days
- __initialUnlockTimeOffset = 2 days
- duration = 7 days
- Now the `block.timestamp = X + 7 days`

Let us check the validations in the _vested method one by one:
- start = X > 0
- block.timestamp = X + 7 > X + 1
- block.timestamp = X + 7 < X + 1 + 7 

All the checks can be passed, and the returned amount 
```
= (_totalAmount * (block.timestamp - _start)) / _duration 
=  _totalAmount * ( X + 7 - (X + 1 - 2)) / 7 
= _totalAmount * 8/7 > _totalAmount
```

## Recommendation
Change the https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/tokens/Vesting.sol#L271
to `if (block.timestamp >= _start - __initialUnlockTimeOffset + _duration) return _totalAmount;`



## Assessed type

Math
Tap and rewards tokens in twTAP positions may stuck or be theft by the attacker
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/governance/twTAP.sol#L547-L551
https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/tokens/TapTokenReceiver.sol#L130-L137
https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/tokens/TapTokenReceiver.sol#L146-L152


# Vulnerability details

The external functions `exitPosition` and `claimRewards` of the twTAP use `_requireClaimPermission` function to check the `to` address has the approved permission of the twTAP token. But it doesn't require the msg.sender is the to address param. So anyone can call the `exitPosition` function to withdraw all the unlocked TAP to approved addresses, or call the `claimRewards` function to send the rewards token.

The main issue is that, to achieve omni-chain operations for claim and exit, users need to have given TwTAP allowance to the TapToken(Module: TapTokenReceiver). So the attacker can send unlocked Tap and rewards to the TapToken address.

```
    function _requireClaimPermission(address _to, uint256 _tokenId) internal view {
        if (!_isApprovedOrOwner(_to, _tokenId) && !isERC721Approved(_ownerOf(_tokenId), _to, address(this), _tokenId)) {
            revert NotApproved(_tokenId, msg.sender);
        }
    }
```

## Proof of Concept 
This behavior can be explicitly found in the comments:
```
     * @dev !!! The user needs to have given TwTAP allowance to this contract  !!!
     * @dev !!! The user needs to have given TwTAP allowance to this contract in order to exit  !!!

```

## impact
1. The rewards token will stuck in the TapToken address forever.
2. The unlocked Tap will be burnt in the emitForWeek function as boostedTAP, and then distribute in the next epoch.

# Recommendation
Make sure the msg.sender is the to address.


## Assessed type

DoS