xanoutas/solidity-security-findings huggingface dataset:
Attacker can create an DOS for every composed messages that contains permit message type by front-running 
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/tapiocaOmnichainEngine/TapiocaOmnichainReceiver.sol#L300-L317
 https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/tapiocaOmnichainEngine/extension/TapiocaOmnichainExtExec.sol#L59-L67


# Vulnerability details

## Description
The Tapioca protocol facilitates multiple LayerZero composed messages by concatenating them into a single message and executing all of them within the `TapiocaOmnichainReceiver.lzCompose()` function.

There are various types of composed messages supported by Tapioca, one of which is the permit message (`msgType = [500, 505]`). These messages are executed through the internal function `TapiocaOmnichainReceiver._extExec()` corresponding to the token type required by the sender.

```solidity
function _extExec(uint16 _msgType, bytes memory _data) internal returns (bool) {
    if (_msgType == MSG_APPROVALS) {
        toeExtExec.erc20PermitApproval(_data);
    } else if (_msgType == MSG_NFT_APPROVALS) {
        toeExtExec.erc721PermitApproval(_data);
    } else if (_msgType == MSG_PEARLMIT_APPROVAL) {
        toeExtExec.pearlmitApproval(_data);
    } else if (_msgType == MSG_YB_APPROVE_ALL) {
        toeExtExec.yieldBoxPermitAll(_data);
    } else if (_msgType == MSG_YB_APPROVE_ASSET) {
        toeExtExec.yieldBoxPermitAsset(_data);
    } else if (_msgType == MSG_MARKET_PERMIT) {
        toeExtExec.marketPermit(_data);
    } else {
        return false;
    }
    return true;
}
```

However, the permit transaction can be executed by anyone, not just the `TapiocaOmnichainReceiver` contract. Consequently, an attacker can front-run the permit() call before the execution of the permit message from the `lzCompose()` function. This results in a revert on the compose message transaction due to the token approval already being granted (increasing the permit nonce of the token). As a result, none of the messages encoded in the composed message with the permit messages, will be executed.

## Impact
DoS attacks can be launched against composed messages that include permit messages. 

## Tools Used
Manual review

## Recommended Mitigation Steps
Consider implementing a low-level call to handle the success of the permit function. If the permit function fails, you can then proceed to the next message instead of reverting the transaction.


## Assessed type

Timing
User can't exercise the option with `paymentToken` that has decimals > 18
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
User can't exercise their options with some set of tokens with more than 18 decimals 

## Proof of Concept
The function `AirdropBreaker._getDiscountedPaymentAmount()` is utilized to compute the discounted payment amount for a given OTC amount in USD.

```solidity
/// link = 

function _getDiscountedPaymentAmount(
    uint256 _otcAmountInUSD,
    uint256 _paymentTokenValuation,
    uint256 _discount,
    uint256 _paymentTokenDecimals
) internal pure returns (uint256 paymentAmount) {
    function _getDiscountedPaymentAmount(
        uint256 _otcAmountInUSD,
        uint256 _paymentTokenValuation,
        uint256 _discount,
        uint256 _paymentTokenDecimals
    ) internal pure returns (uint256 paymentAmount) {
        if (_paymentTokenValuation == 0) revert PaymentTokenValuationNotValid();
        
        // Calculate payment amount
        uint256 rawPaymentAmount = _otcAmountInUSD / _paymentTokenValuation;
        paymentAmount = rawPaymentAmount - muldiv(rawPaymentAmount, _discount, 100e4); // 1e4 is discount decimals, 100 is discount percentage

        paymentAmount = paymentAmount / (10 ** (18 - _paymentTokenDecimals));
    }
}
```

Towards the end of the function, it attempts to normalize the `paymentAmount` from 18 to its own decimal representation. However, it lacks any check to ensure that `_paymentTokenDecimals` is greater than 18, which may result in a reversion due to an underflow error.

The same issue was identified in a previous contest, but the fix was only implemented for the `TapiocaOptionBreaker` contract and not for this contract.
* https://github.com/code-423n4/2023-07-tapioca-findings/issues/879

## Tools Used
Manual review

## Recommended Mitigation Steps
Modify function as follows: 
```solidity=
function _getDiscountedPaymentAmount(
    uint256 _otcAmountInUSD,
    uint256 _paymentTokenValuation,
    uint256 _discount,
    uint256 _paymentTokenDecimals
) internal pure returns (uint256 paymentAmount) {
    if (_paymentTokenValuation == 0) revert PaymentTokenValuationNotValid();
    // Calculate payment amount
    uint256 rawPaymentAmount = _otcAmountInUSD / _paymentTokenValuation;
    paymentAmount = rawPaymentAmount - muldiv(rawPaymentAmount, _discount, 100e4); // 1e4 is discount decimals, 100 is discount percentage

    if (_paymentTokenDecimals <= 18) {
      paymentAmount = paymentAmount / (10 ** (18 - _paymentTokenDecimals));
    }
    else {
      paymentAmount = paymentAmount * (10 ** (_paymentTokenDecimals - 18));
    }
}
```



## Assessed type

Token-Transfer
The attacker can exercise the TAP option twice by just locking their position for slightly more than one week
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/options/TapiocaOptionBroker.sol#L512-L514
https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/options/TapiocaOptionBroker.sol#L318-L320
https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/options/TapiocaOptionBroker.sol#L369-L370


# Vulnerability details

## Description
Both timestamp and epoch logic are utilized within the `TapiocaOptionBroker` contract to determine if a position is active:

- In the `exitPosition()` function, a position can be exited when `block.timestamp` exceeds `lock.lockTime + lock.lockDuration`.
- Meanwhile, in the `exerciseOption()` function, the position is considered active if `epoch` is less than or equal to `_timestampToWeek(lock.lockTime + lock.lockDuration)`.

```solidity=
function _timestampToWeek(uint256 timestamp) internal view returns (uint256) {
    return ((timestamp - emissionsStartTime) / EPOCH_DURATION);
}
```

Additionally, the `epoch` storage value can be incremented by 1 when a week has passed by invoking the `newEpoch()` function. This function is open for anyone to call, enabling an attacker to exploit the system by creating a lock position slightly longer than a week (e.g., `lockDuration = WEEK + 100 seconds`) and locking it near the end of an epoch to exercise the option for two weeks.

Consider the following scenario for the attacker's strategy:

1. Assume `emissionsStartTime = 0`.
2. At `block.timestamp = EPOCH_DURATION - 10`, the attacker calls `participate()` with a position duration of `EPOCH_DURATION + 10`.
3. At `block.timestamp = EPOCH_DURATION`, the `newEpoch()` function is invoked, incrementing `epoch` to 1.
4. At `block.timestamp = 2 * EPOCH_DURATION - 10`, a week after the attacker's `participate()` call, they execute `exerciseOption()` to obtain the first option.
5. At `block.timestamp = 3 * EPOCH_DURATION`:
    - `newEpoch()` is invoked, advancing `epoch` to 2.
    - The attacker calls `exerciseOption()` to obtain the second option (valid since `epoch = 3 = _timestampToWeek(lock.lockTime + lock.lockDuration)`).
6. The attacker calls `exitPosition` to exit their position.

As demonstrated, by locking their position for slightly more than a week, the attacker can acquire two options instead of one.

## Impact
* The attacker can claim x+1 options by simply locking their position for slightly more than x weeks. 
* Additionally, the tap token to be exercised at epoch x+1 will be diluted by the attacker. 

## Tools Used
Manual review

## Recommended Mitigation Steps
Ensure that positions can participate in the option broker, they must be divisible by `EPOCH_DURATION`.


## Assessed type

Timing
Use safeTransfer/safeTransferFrom consistenly instead of transfer/transferFrom 
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact 
It is good to add a `require()` statement that checks the return value of token transfers or to use something like OpenZeppelin’s `safeTransfer/safeTransferFrom` unless one is sure the given token reverts in case of a failure. Failure to do so will cause silent failures of transfers and affect token accounting in contract.

While most places use a require or `safeTransfer/safeTransferFrom`, there are one missing case in the function `TapTokenReceiver._claimTwpTapRewardsReceiver()` when the contract try to send the dust reward to the `sendTo_` address. 
 
## Tools Used
Manual review 
  
## Recommended Mitigation Steps
Consider using safeTransfer/safeTransferFrom consistently.


## Assessed type

Token-Transfer
Unable to decode the `duration` parameter in the function `TapTokenC::decodeLockTwpTapDstMsg()`
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Description
The function `decodeLockTwpTapDstMsg()` is utilized to decipher an encoded message intended for the `lockTwTapPosition()` operation.

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/tokens/TapTokenCodec.sol#L62-L81
```solidity=
function decodeLockTwpTapDstMsg(bytes memory _msg)
    internal
    pure
    returns (LockTwTapPositionMsg memory lockTwTapPositionMsg_)
{
    // TODO bitwise operators
    // Offsets
    uint8 userOffset_ = 20;
    uint8 durationOffset_ = 32;


    // Decoded data
    address user = BytesLib.toAddress(BytesLib.slice(_msg, 0, userOffset_), 0);


    uint96 duration = BytesLib.toUint96(BytesLib.slice(_msg, userOffset_, durationOffset_), 0);


    uint256 amount = BytesLib.toUint256(BytesLib.slice(_msg, durationOffset_, _msg.length - durationOffset_), 0);


    // Return structured data
    lockTwTapPositionMsg_ = LockTwTapPositionMsg(user, duration, amount);
}
```

The input `_msg` is encoded using the `abi.encodePacked()` operation with three parameters: `user` of type `address`, `duration` of type `uint96`, and `amount` of type `uint256`. Thus, the value of each parameter can be obtained by extracting the bytes within specific ranges of the `_msg` message:
- `user` corresponds to bytes `[0, 20)`
- `duration` corresponds to bytes `[20, 32)`
- `amount` corresponds to bytes `[32, 64)`

To achieve this, the function employs the `BytesLib.slice()` function from the `BytesLib` library to extract the byte ranges of the `_msg`.

https://github.com/GNSPS/solidity-bytes-utils/blob/df88556cbbc267b33a787a3a6eaa32fd7247b589/contracts/BytesLib.sol#L228-L232
```solidity=
function slice(
    bytes memory _bytes,
    uint256 _start,
    uint256 _length
) 
    internal
    pure
    returns (bytes memory)
```

However, there's an issue when handling the `duration` parameter, which is a `uint96` variable (96 bits = 12 bytes). The function erroneously uses `durationOffset_ = 32` as the `_length` parameter when passing it to the `BytesLib.slice()` function. Consequently, when the casting `BytesLib.toUint96()` attempts to convert a 32-byte value into a 12-byte value, it triggers a revert.

## Impact
The function `_lockTwTapPositionReceiver` fails to execute as the transaction reverts.

## Tools Used
Manual review

## Recommended Mitigation Steps
When decoding the value of the variable `duration` in the function `TapTokenC::decodeLockTwpTapDstMsg`, set `_length` to 12

```solidity=
uint96 duration = BytesLib.toUint96(BytesLib.slice(_msg, userOffset_, 12), 0);
```


## Assessed type

en/de-code
[M15] Magnetar's `mintBBLendSGLLockTOLP` reverts when `lock` is set to false
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarMintCommonModule.sol#L68-L93


# Vulnerability details

## Impact

The `mintBBLendSGLLockTOLP` function in Magnetar is designed to mint USDO tokens from bigbang, deposit them to singularity, lock the liquidity in the TOLP contract and then participate in the TOB contract.

The function is designed to be modular, so the user can choose to skip any of the steps and still have the other execute. The issue is that if the user decides not lock in the TOLP contract, the function will revert since it is pulling tokens from the wrong address.

After the market operations, the function does two operations for locking, as shown below.

```solidity
uint256 tOLPTokenId = _lockOnTOB(
    data.lockData,
    yieldBox_,
    fraction,
    data.participateData.participate,
    data.user,
    data.externalContracts.singularity
);

if (data.participateData.participate) {
    _participateOnTOLP(data.participateData, data.user, data.lockData.target, tOLPTokenId);
}
```

In the `_lockOnTOB` function, there is a check which allows the user to skip this step based on their input.

```solidity
if (lockData.lock) {
    //...
}
```

However, if this step is skipped, then the TOLP NFT position will still be with the user, and thus needs to be pulled from the user for the `participate` step. But in the `participate` step, we see that the code expects the token to be with the MAgnetar contract already.

```solidity
IERC721(lockDataTarget).approve(participateData.target, tOLPTokenId);
uint256 oTAPTokenId = ITapiocaOptionBroker(participateData.target).participate(tOLPTokenId);

address oTapAddress = ITapiocaOptionBroker(participateData.target).oTAP();
IERC721(oTapAddress).safeTransferFrom(address(this), user, oTAPTokenId, "0x");
```

This would only be true if the previous step's `lock` function had run. This is because when locking the tokens, the Magnetar contract receives the NFT position if participate function is set to be run.

SO if the `lock` function is skipped, the `participate` function will revert since the NFT position is still with the user.

## Proof of Concept

During the `lock` function call, we see that the Magnetar contract tries to credit itself the NFT tokens.

```solidity
tOLPTokenId = ITapiocaOptionLiquidityProvision(lockData.target).lock(
    participate ? address(this) : user, singularityAddress, lockData.lockDuration, lockData.amount
);
```

However, if the lock functionality is skipped, then the Magnetar contract will not own the tokens since this step will not be run. Users will be forced to raw send the NFT position to the magnetar contract which is unsafe since the Magnetar contract is not designed to hold tokens.

## Tools Used

Manual Review

## Recommended Mitigation Steps

If `lockData.lock` was true, the `_participateOnTOLP` function should pull the NFT positions from the user to the Magnetsar contract.
[M14] Missing approval in Mangetar's `_lockOnTOB` function results in broken functionality
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarMintCommonModule.sol#L116-L117


# Vulnerability details

## Impact

The function `_lockOnTOB` is used to deposit SGL liquidity tokens into the yieldbox and then use them to create a lock position. The issue is that the function does not give approval to the yieldbox to use its tokens, which will lead to an insufficient allowance based revert.

The function first takes out tokens from the user with the `_extractTokens` function, and then tries to send them off to the yieldbox to deposit them.

```solidity
_extractTokens(user, singularityAddress, fraction);
yieldBox_.depositAsset(tOLPSglAssetId, address(this), address(this), fraction, 0);
```

The issue is that the contract never gave approval to the yieldbox to use these tokens. So the yieldbox does not have permission to move out these tokens from the contract, and thus this function will always fail.

## Proof of Concept

The lack of approval can be seen in the code itself. We can see the correct pattern being used in the other function `_depositYBLendSGL`, where the approval is first given out.

```solidity
sglAssetAddress.safeApprove(address(yieldBox_), depositData.amount);
yieldBox_.depositAsset(sglAssetId, address(this), user, depositData.amount, 0);
```

The `safeApprove` call is missing in the `_lockOnTOB` function, and thus the function will always fail.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Hand out the approval by using a `safeApprove` call like done in the other functions.



## Assessed type

Invalid Validation
[M13] Magnetar's `depositYBLendSGLLockXchainTOLP` fails if deposit is skipped
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarAssetXChainModule.sol#L70-L116


# Vulnerability details

## Impact

The function `depositYBLendSGLLockXchainTOLP` in the `MagnetarAssetXChainModule.sol` contract is responsible for doing a bunch of operations.

1. Deposits asset to yieldbox
2. Lends to SGL market
3. Wraps liquidity tokens into TOFT
4. Sends TOFT crosschain

The functions are designed so that each operation is independent of the other. Thus a user can decide to skip step 2 and still have all other functions run. This is similar to all the other magnetar module functions as well.

The issue here is that if a user skips steps 1 and 2, step 3 will revert.

A user can decide to not provide liquidity to the SGL market by setting `data.lendAmount=0` and the deposit flag to false. Then, the `_depositYBLendSGL` function will skip the liquidity addition step.

```solidity
fraction = 0;
if (lendAmount == 0 && depositData.deposit) {
    lendAmount = depositData.amount;
}
if (lendAmount > 0) {
    uint256 lendShare = yieldBox_.toShare(sglAssetId, lendAmount, false);
    fraction = ISingularity(singularityAddress).addAsset(user, user, false, lendShare);
}
```

Since this part is skipped, the `fraction` is evaluated to 0, and the next operation is called with 0 as the parameter.

```solidity
uint256 fraction =
    _depositYBLendSGL(data.depositData, data.singularity, IYieldBox(yieldBox), data.user, data.lendAmount);

// wrap SGL receipt into tReceipt
// ! User should approve `address(this)` for `IERC20(data.singularity)` !
//@audit fraction can be 0 if the previous steps were skipped
uint256 toftAmount = _wrapSglReceipt(IYieldBox(yieldBox), data.singularity, data.user, fraction, data.assetId);
```

The issue is that TOFT wrapping reverts if 0 value is passed. The `_wrapSglReceipt` ends up calling the `wrap` as shown below.

```solidity
toftAmount = ITOFT(tReceiptAddress).wrap(address(this), address(this), fraction);
```

The `wrap` function reverts on 0 value passed.

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

```solidity
if (_amount == 0) revert TOFT_NotValid();
```

So users cannot skip the liquidity addition part and only do the cross-chain TOFT wrapping and sending part.

## Proof of Concept

The issue arises from the fact that the `_wrapSglReceipt` function is always run, even if the `fraction` value is 0. This conflicts with the `wrap` function in the TOFT contract which reverts on 0 value.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Add a check so that the wrapping is only done if `fraction>0`.



## Assessed type

Other
[M12] Magnetar unwrap operations broken due to bad ownership and checks
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

The `_processWrapOperation` function can be triggered on the Magnetar contract to wrap/unwrap the user's tokens into or out of the TOFT contracts. This function allows 2 selectors, `wrap` and `unwrap` to be called.

The issue is that `unwrap` function has 2 issues which prevent it from working properly.

Below is the code from the BaseTOFT.sol contract, showing the implementation of the `unwrap` function.

```solidity
function _unwrap(address _toAddress, uint256 _amount) internal virtual {
    _burn(msg.sender, _amount);
    vault.withdraw(_toAddress, _amount);
}
```

As seen here, the token is burnt from the `msg.sender` address. However, the `_processWrapOperation` function in the Magnetar contract does not transfer out the token from the user's address to itself before calling `_unwrap`.

```solidity
if (funcSig == ITOFT.wrap.selector || funcSig == ITOFT.unwrap.selector) {
    /// @dev Owner param check. See Warning above.
    _checkSender(abi.decode(_actionCalldata[4:36], (address)));
    _executeCall(_target, _actionCalldata, _actionValue, _allowFailure);
    return;
}
```

So the Magnetar contract isnt in posession of the token that the TOFT contract is trying to burn. The Magnetar contract itself is not designed to hold user tokens, since anyone can claim them, so user's should not send their tokens to the Magnetar contract manually and then call this function, since MEV bots can steal tokens from this contract by just calling the unwrap function before them. Due to this, there is no way for Magnetar to actually unwrap the tokens.

Secondly, the `_checkSender` function is used to check the first passed address against `msg.sender`. the issue is that the first address passed to the `unwrap` function is the destination address, not the owner's address. The owner is assumed to be `msg.sender`. So this contract essentially only makes sure that the `msg.sender` matches the destination of the unwrapped tokens, which is not a useful check.

Since these 2 issues break the functionality of this function, this is a medium severity issue.

## Proof of Concept

The fact that Magnetar isnt designed to hold tokens is evident from the fact that any user can just call the unwrap function from magnetar and burn up any tokens it is currently holding. So the absence of the transferring of the token to the Magnetar contract itself causes an issue.

## Tools Used

Manual Review

## Recommended Mitigation Steps

The `unwrap` functionality should transfer the token from the `msg.sender` to itself first. This will ensure both that the caller owns the token and that the token is in the possession of the Magnetar contract. The `_checkSender` check for the `unwrap` case is unnecessary and prevents users from choosing a destination address.



## Assessed type

Invalid Validation