xanoutas/solidity-security-findings huggingface dataset:
Users can overclaim vesting rewards, due to wrong calculation with `__initialUnlockTimeOffset`
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

## Impact
Users can claim significantly more rewards from Vesting than supposed to.

## Proof of Concept
When vesting starts, the owner can set an `initialUnlockAmount` - an amount of funds to be instantly unlocked. Based on the duration of the vesting, it is recalculated as `__initialUnlockTimeOffset`
```solidity
    function _computeTimeFromAmount(uint256 _start, uint256 _totalAmount, uint256 _amount, uint256 _duration)
        internal
        pure
        returns (uint256)
    {
        return _start - (_start - ((_amount * _duration) / _totalAmount)); // @audit - please change it to simply (_amount * _duration) / _totalAmount) lol   }
```

However, let's look at the `_vested` function
```soldity
    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
    }
```

Since the 'fully vested' check is done before adjusting `_start` for the `initialUnlockTimeOffset` this allows for `block.timestamp - _start` to return value higher than `_duration`

Example.
1. Vesting time is 2 months.
2. initialUnlockTimeOffset is 1 months
3. Vesting starts and 1.99 months go by. User calls claim.
4. `if (block.timestamp >= _start + _duration)` returns false, as only 1.99 months have passed 
5. `(_totalAmount * (block.timestamp - _start)) / _duration` returns `totalAmount * 2.99 months / 2 months` or approx ~1.5x totalAmount


## Tools Used
Manual review


## Recommended Mitigation Steps
put the fully vested check after adjusting for offset


## Assessed type

Error
Malicious users can use arbitrary addresses in composeMsg to steal other users' assets
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
In *XChainModule, users can perform cross-chain operations through compose call in the following calls.
```
     *  step 1: magnetar.mintBBLendXChainSGL (chain A) -->
     *         step 2: IUsdo compose call calls magnetar.depositYBLendSGLLockXchainTOLP (chain B) -->
     *              step 3: IToft(sglReceipt) compose call calls magnetar.lockAndParticipate (chain X)
```
In mintBBLendXChainSGL, the parameters used by depositYBLendSGLLockXchainTOLP will be constructed to composeMsg,
```solidity
    function mintBBLendXChainSGL(CrossChainMintFromBBAndLendOnSGLData memory data) public payable {
        // Check sender
        _checkSender(data.user);

        address yieldBox = IMarket(data.bigBang).yieldBox();

        // if `mint` was requested the following actions are performed:
        //  - extracts & deposits collateral to YB
        //  - performs bigBang_.addCollateral
        //  - performs bigBang_.borrow
        if (data.mintData.mint) {
            _depositYBBorrowBB(data.mintData, data.bigBang, IYieldBox(yieldBox), data.user, data.marketHelper);
        } // USDOybERC1155

        // decode `composeMsg` and re-encode it with updated params
        (uint16 msgType_,, uint16 msgIndex_, bytes memory tapComposeMsg_, bytes memory nextMsg_) =
            TapiocaOmnichainEngineCodec.decodeToeComposeMsg(data.lendSendParams.lzParams.sendParam.composeMsg);

        DepositAndSendForLockingData memory lendData = abi.decode(tapComposeMsg_, (DepositAndSendForLockingData));
        lendData.lendAmount = data.mintData.mintAmount;
        // @audit: should override lenddata.user = data.lendSendParams.lzParams.sendParam.to
``` 
and in depositYBLendSGLLockXchainTOLP, the parameters used by lockAndParticipate will be constructed to composeMsg.
```solidity
    function depositYBLendSGLLockXchainTOLP(DepositAndSendForLockingData memory data) public payable {
        // Check sender
        _checkSender(data.user);

        address yieldBox = IMarket(data.singularity).yieldBox();

        // if `depositData.deposit`:
        //      - deposit SGL asset to YB for `data.user`
        // if `lendAmount` > 0:
        //      - add asset to SGL
        uint256 fraction =  // sglAsset(USDO) -> ybERC1155 -> sgl
            _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)` !  sgl -> toft
        uint256 toftAmount = _wrapSglReceipt(IYieldBox(yieldBox), data.singularity, data.user, fraction, data.assetId);

        data.lockAndP
Magnetar: _processOFTOperation has no check selector, users can forge _action.id to bypass the check
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
Magnetar.burst will select the call that the user wants to perform based on action.id. In these calls, the selector will be checked, and for special calls, the caller will be checked.
```solidity
    function burst(MagnetarCall[] calldata calls) external payable {
        uint256 valAccumulator;

        uint256 length = calls.length;

        for (uint256 i; i < length; i++) {
            MagnetarCall calldata _action = calls[i];
            if (!_action.allowFailure) {
                require(
                    _action.call.length > 0,
                    string.concat("Magnetar: Missing call for action with index", string(abi.encode(i)))
                );
            }
            valAccumulator += _action.value;

            /// @dev Permit on YB, or an SGL/BB market
            if (_action.id == MagnetarAction.Permit) {
```
The problem here is that neither the selector nor the caller is checked in _processOFTOperation, which allows the user to forge the action.id and thus bypass the caller check.
```solidity
    function _processOFTOperation(
        address _target,
        bytes calldata _actionCalldata,
        uint256 _actionValue,
        bool _allowFailure
    ) private {
        if (!cluster.isWhitelisted(0, _target)) revert Magnetar_NotAuthorized(_target, _target);
        _executeCall(_target, _actionCalldata, _actionValue, _allowFailure);
    }
```
For example, when the user calls TOFT.wrap(), the caller will be checked in _processWrapOperation to see if the caller is consistent with the parameter.

However, the user can make _action.id == MagnetarAction.OFT and call TOFT.wrap() in _processOFTOperation, which bypasses the caller check, allowing users to wrap the approved TOFT to themselves.

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

## Tools Used
None
## Recommended Mitigation Steps
It is recommended to check the selector in _processOFTOperation


## Assessed type

Context
TapTokenReceiver: Any user can cross-chain to claim other users' rewards
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Users can lock their TapToken into twTap to get rewards, and it supports cross-chain rewards collection.

When a user initiates a cross-chain compose call to collect the reward, in TapTokenReceiver._claimTwpTapRewardsReceiver(), it only checks that address(this) has been approved, but does not check if _srcChainSender has been approved, which results in any user being able to initiate a cross-chain compose call to collect rewards from any approved twTap to themselves.
```solidity
    function _claimTwpTapRewardsReceiver(bytes memory _data) internal virtual twTapExists {
        ClaimTwTapRewardsMsg memory claimTwTapRewardsMsg_ = TapTokenCodec.decodeClaimTwTapRewardsMsg(_data);

        // Claim rewards, make sure to have approved this contract on TwTap.
        uint256[] memory claimedAmount_ = twTap.claimRewards(claimTwTapRewardsMsg_.tokenId, address(this));
```
## Proof of Concept
https://github.com/Tapioca-DAO/tap-token//blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/tokens/TapTokenReceiver.sol#L152-L156
## Tools Used
None
## Recommended Mitigation Steps
It is recommended to check that the _srcChainSender is approved in the _claimTwpTapRewardsReceiver.
```
    function _toeComposeReceiver(uint16 _msgType, address _srcChainSender, bytes memory _toeComposeMsg)
        internal
        override
        returns (bool success)
    {
        if (_msgType == MSG_LOCK_TWTAP) {
            _lockTwTapPositionReceiver(_srcChainSender, _toeComposeMsg);
        } else if (_msgType == MSG_UNLOCK_TWTAP) {
            _unlockTwTapPositionReceiver(_toeComposeMsg);
        } else if (_msgType == MSG_CLAIM_REWARDS) {
-           _claimTwpTapRewardsReceiver(_toeComposeMsg);
+           _claimTwpTapRewardsReceiver(_srcChainSender, _toeComposeMsg);
...
-   function _claimTwpTapRewardsReceiver(bytes memory _data) internal virtual twTapExists {
+   function _claimTwpTapRewardsReceiver(address _srcChainSender, bytes memory _data) internal virtual twTapExists {
        ClaimTwTapRewardsMsg memory claimTwTapRewardsMsg_ = TapTokenCodec.decodeClaimTwTapRewardsMsg(_data);
+       require(twTap.ownerOf(claimTwTapRewardsMsg_.tokenId) == _srcChainSender,"...");
        // Claim rewards, make sure to have approved this contract on TwTap.
        uint256[] memory claimedAmount_ = twTap.claimRewards(claimTwTapRewardsMsg_.tokenId, address(this));
```


## Assessed type

Context
Lack of input validation in data.externalData.marketHelper in Option module exitPositionAndRemoveCollateral leads to theft of fund
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Lack of input validation in data.externalData.marketHelper in Option module exitPositionAndRemoveCollateral leads to theft of fund

## Proof of Concept

If we take a look at other module implementation,

the code validate if the [market helper address](https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarCollateralModule.sol#L63) is whitelisted

```
function depositAddCollateralAndBorrowFromMarket(DepositAddCollateralAndBorrowFromMarketData memory data)
    public
    payable
{
    // Check sender
    _checkSender(data.user);

    // Check
    if (!cluster.isWhitelisted(0, data.market)) {
        revert Magnetar_TargetNotWhitelisted(data.market);
    }
    if (!cluster.isWhitelisted(0, data.marketHelper)) {
        revert Magnetar_TargetNotWhitelisted(data.marketHelper);
    }
```

But in the Option module, the market helper address is not validated and the whitelist check if missing.

this leads to theft of fund because attacker can bypass [the check _checkSender](https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarOptionModule.sol#L60)

```
   function exitPositionAndRemoveCollateral(ExitPositionAndRemoveCollateralData memory data) public payable {
        // Check sender
        _checkSender(data.user);
```

the _checkSender validate that the data.user is authorized to trigger certain action.

By validating the data.user, user A should only trigger borrow / repay / add / remove asset when data.user match user A address.

```
  function _checkSender(address _from) internal view {
        if (_from != msg.sender && !cluster.isWhitelisted(0, msg.sender)) {
            revert Magnetar_NotAuthorized(msg.sender, _from);
        }
    }
```

but because the lack of market helper input validation, 

Attacker can set victim's address and [remove collateral from victim's address](https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarOptionModule.sol#L197)


```
// performs a BigBang removeCollateral operation
// if `data.removeAndRepayData.collateralWithdrawData.withdraw` withdraws by using the `withdrawTo` method
if (data.removeAndRepayData.removeCollateralFromBB) {
    uint256 _collateralId = bigBang_.collateralId();
    uint256 collateralShare = yieldBox_.toShare(_collateralId, data.removeAndRepayData.collateralAmount, false);
    address removeCollateralTo =
        data.removeAndRepayData.collateralWithdrawData.withdraw ? address(this) : data.user;

    (Module[] memory modules, byte
Lack of handling of user's added collateral after mintBBLendSGLLockTOLP
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarMintModule.sol#L60
https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarMintModule.sol#L49


# Vulnerability details

## Impact

Lack of handling of user's added collateral after mintBBLendXChainSGL

## Proof of Concept

In MagnetarMintCommonModule.sol, [there is a function](https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarMintModule.sol#L60):

```
   function _depositYBBorrowBB(
        IMintData memory mintData,
        address bigBangAddress,
        IYieldBox yieldBox_,
        address user,
        address marketHelper
    ) internal {
```

the function involes the logic:

```
if (mintData.collateralDepositData.deposit) {
    mintData.collateralDepositData.amount =
        _extractTokens(user, bbCollateralAddress, mintData.collateralDepositData.amount);
    bbCollateralShare = yieldBox_.toShare(bbCollateralId, mintData.collateralDepositData.amount, false);

    bbCollateralAddress.safeApprove(address(yieldBox_), mintData.collateralDepositData.amount);
    yieldBox_.depositAsset(
        bbCollateralId, address(this), address(this), mintData.collateralDepositData.amount, 0
    );
}

// add collateral to BB
if (mintData.collateralDepositData.amount > 0) {
    _setApprovalForYieldBox(address(bigBang_), yieldBox_);

    (Module[] memory modules, bytes[] memory calls) = IMarketHelper(marketHelper).addCollateral(
        mintData.collateralDepositData.deposit ? address(this) : user,
        user,
        false,
        mintData.collateralDepositData.amount,
        bbCollateralShare
    );
    bigBang_.execute(modules, calls, true);
}

// mints from BB
{
    (Module[] memory modules, bytes[] memory calls) =
        IMarketHelper(marketHelper).borrow(user, user, mintData.mintAmount);
    bigBang_.execute(modules, calls, true);
}
```

the logic involves deposit asset in yield  box to mint yield box shares,

add yield box shares as collateral

and borrow the fund from big bang market.

how this function is used? In the function [mintBBLendSGLLockTOLP](https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/Magnetar/modules/MagnetarMintModule.sol#L49)

```
     // if `mint` was requested the following actions are performed:
        //  - extracts & deposits collateral to YB
        //  - performs bigBang_.addCollateral
        //  - performs bigBang_.borrow
        if (data.mintData.mint) {
            _depositYBBorrowBB(
                data.mintData, data.externalContracts.bigBang, yieldBox_, data.user, data.externalContracts.marketHelper
            );
        }

        // if `depositData.deposit`:
        //      - deposit SGL asset to YB for `data.user`
        // if `lendAmount` > 0:
        //      - add ass
Lack of validation for target address and arbitrary call when _action.id == MagnetarAction.OFT leads to loss of asset
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Access control for when _action.id == MagnetarAction.OFT is missing in Magnetar

## Proof of Concept

This is the current implementation of Magnetar.sol in function burst

```
 function burst(MagnetarCall[] calldata calls) external payable {
        uint256 valAccumulator;

        uint256 length = calls.length;

        for (uint256 i; i < length; i++) {
            MagnetarCall calldata _action = calls[i];
            if (!_action.allowFailure) {
                require(
                    _action.call.length > 0,
                    string.concat("Magnetar: Missing call for action with index", string(abi.encode(i)))
                );
            }
            valAccumulator += _action.value;

            /// @dev Permit on YB, or an SGL/BB market
            if (_action.id == MagnetarAction.Permit) {
                _processPermitOperation(_action.target, _action.call, _action.allowFailure);
                continue; // skip the rest of the loop
            }

            /// @dev Wrap/unwrap singular operations
            if (_action.id == MagnetarAction.Wrap) {
                _processWrapOperation(_action.target, _action.call, _action.value, _action.allowFailure);
                continue; // skip the rest of the loop
            }

            /// @dev Market singular operations
            if (_action.id == MagnetarAction.Market) {
                _processMarketOperation(_action.target, _action.call, _action.value, _action.allowFailure);
                continue; // skip the rest of the loop
            }
```

If the _action.id is MagnetarAction.Permit or WRAP or MARKET or TAPTOKEN

the code process option is called, the general logic is the code validate if the target is whitelisted,

then validate if the msg.sender is authorized

```
      if (!cluster.isWhitelisted(0, _target)) revert Magnetar_NotAuthorized(_target, _target);

        /// @dev owner address should always be first param.
        // permitAction(bytes,uint16)
        // permit(address owner...)
        // revoke(address owner...)
        // permitAll(address from,..)
        // permit(address from,...)
        // setApprovalForAll(address from,...)
        // setApprovalForAsset(address from,...)

        bytes4 funcSig = bytes4(_actionCalldata[:4]);
        if (
            funcSig == IPermitAll.permitAll.selector || funcSig == IPermitAll.revokeAll.selector
                || funcSig == IPermit.permit.selector || funcSig == IPermit.revoke.selector
                || funcSig == IYieldBox.setApprovalForAll.selector || funcSig == IYieldBox.setApprovalForAsset.selector 
                || funcSig == IERC20.approve.selector || funcSig == IPearlmit.approve.selector
                || funcSig == IERC721.approve.selector 
        ) {
            // @audit
            // check sender is missing
Does not handle the address(0) token transfer when claim reward when reward token index is 0
Audit: 2024-02-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/governance/twTAP.sol#L138
https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/governance/twTAP.sol#L566
https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/governance/twTAP.sol#L478


# Vulnerability details

## Impact

claimRewards does not handle the address(0) token in the first index,

and block reward claim.

## Proof of Concept


When claiming rewards, we need to iterate over the rewardToken array and transfer each reward token one by one.

```
  function _claimRewards(uint256 _tokenId, address _to) internal returns (uint256[] memory amounts_) {
        amounts_ = claimable(_tokenId);
        uint256 len = amounts_.length;
        unchecked {
            for (uint256 i; i < len; ++i) {
                uint256 amount = amounts_[i];
                if (amount > 0) {
                    // Math is safe: `amount` calculated safely in `claimable()`
                    claimed[_tokenId][i] += amount;
                    rewardTokens[i].safeTransfer(_to, amount);
                }
            }
        }
    }
```

but the first index of the rewardTokens array is address(0), 

the address(0) is pushed into array in the constructor of the twTAP.sol contract

```
    constructor(address payable _tapOFT, IPearlmit _pearlmit, address _owner)
        ERC721NftLoader("Time Weighted TAP", "twTAP", _owner)
        ERC721Permit("Time Weighted TAP")
        PearlmitHandler(_pearlmit)
    {
        tapOFT = TapToken(_tapOFT);
        creation = block.timestamp;

        rewardTokens.push(IERC20(address(0x0))); // 0 index is reserved
```

calling address(0).safeTransfer(_to, amount) will revert the whole claim transaction and block user from claim all his reward always

this is because we are using the 

```
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
```

from openzeppelin, which does contract existence check

```
 function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
        // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
        // and not revert is the subcall reverts.

        (bool success, bytes memory returndata) = address(token).call(data);
        return
            success && (returndata.length == 0 || abi.decode(returndata, (bool))) && Address.isContract(address(token));
    }
}
```

the same issue presents when anyone wants to call distributeReward

```
  function distributeReward(uint256 _rewardTokenId, uint256 _amount) external nonReentrant {
        if (lastProcessedWeek != currentWeek()) revert AdvanceWeekFirst();
        if (_amount == 0) revert NotValid();
        if (_rewardTokenId == 0) re
Upgraded Q -> 2 from #184 [1712936233288]
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

Judge has assessed an item in Issue #184 as 2 risk. The relevant finding follows:

 Check is always true
https://github.com/Tapioca-DAO/tap-token/blob/050e666142e61018dbfcba64d295f9c458c69700/contracts/governance/twTAP.sol#L301-L302

        if (_timestampToWeek(block.timestamp) > currentWeek()) revert AdvanceEpochFirst(); /// @audit QA for now
Mitigation
https://github.com/Tapioca-DAO/tap-token/blob/050e666142e61018dbfcba64d295f9c458c69700/contracts/governance/twTAP.sol#L464-L465

        if (lastProcessedWeek != currentWeek()) revert AdvanceWeekFirst();
Upgraded Q -> 2 from #184 [1712935796435]
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

Judge has assessed an item in Issue #184 as 2 risk. The relevant finding follows:

 QA - You can get rewards for next epoch, by specifying duration of second + 1
This means you get an extra epoch worth of rewards (can claim even after withdrawing) While not needing to be locked